From ccda836c6e8b19defe198a458b7abfe3cd0190ff Mon Sep 17 00:00:00 2001 From: ZackYoung Date: Sun, 1 Dec 2024 21:07:10 +0800 Subject: [PATCH] [Feature][web] Add resource management to the datastudio page (#3986) Co-authored-by: zackyoungh --- .../CenterTabContent/SqlTask/index.tsx | 12 +- .../src/pages/DataStudio/DvaFunction.tsx | 5 + .../DataStudio/Toolbar/Resource/index.tsx | 396 ++++++++++++++++++ .../pages/DataStudio/Toolbar/ToolbarRoute.tsx | 9 + dinky-web/src/pages/DataStudio/index.tsx | 8 +- .../src/pages/Other/Login/LoginForm/index.tsx | 64 +-- .../SettingCenter/GlobalSetting/model.ts | 4 +- .../SettingCenter/GlobalSetting/service.ts | 6 +- 8 files changed, 464 insertions(+), 40 deletions(-) create mode 100644 dinky-web/src/pages/DataStudio/Toolbar/Resource/index.tsx diff --git a/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx b/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx index 7d29ef1e90..0004a37ca4 100644 --- a/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx +++ b/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx @@ -188,10 +188,6 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { if (taskDetail) { const statement = params.statement ?? taskDetail.statement; const newParams = { ...taskDetail, taskId: params.taskId, statement, mockSinkFunction: true }; - // @ts-ignore - setCurrentState(newParams); - updateCenterTab({ ...props.tabData, params: newParams }); - if (taskDetail.dialect.toLowerCase() === DIALECT.FLINKJAR) { const sqlConvertForm = await flinkJarSqlConvertForm(taskDetail.statement); setSqlForm({ enable: true, ...sqlConvertForm }); @@ -213,6 +209,9 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { } } else { setOriginStatementValue(statement); + // @ts-ignore + setCurrentState(newParams); + updateCenterTab({ ...props.tabData, params: newParams }); if (params?.statement && params?.statement !== taskDetail.statement) { setDiff([{ key: 'statement', server: taskDetail.statement, cache: params.statement }]); setOpenDiffModal(true); @@ -909,7 +908,10 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { })); }} > - + {({ manualInput }) => { diff --git a/dinky-web/src/pages/DataStudio/DvaFunction.tsx b/dinky-web/src/pages/DataStudio/DvaFunction.tsx index 4bae47f2bd..8ff71e7be1 100644 --- a/dinky-web/src/pages/DataStudio/DvaFunction.tsx +++ b/dinky-web/src/pages/DataStudio/DvaFunction.tsx @@ -125,6 +125,11 @@ export const mapDispatchToProps = (dispatch: Dispatch) => { type: CONFIG_MODEL_ASYNC.queryDsConfig, payload: params }), + queryResourceConfig: (params: string) => + dispatch({ + type: CONFIG_MODEL_ASYNC.queryResourceConfig, + payload: params + }), queryResource: () => dispatch({ type: STUDIO_MODEL_ASYNC.queryResource diff --git a/dinky-web/src/pages/DataStudio/Toolbar/Resource/index.tsx b/dinky-web/src/pages/DataStudio/Toolbar/Resource/index.tsx new file mode 100644 index 0000000000..331984a899 --- /dev/null +++ b/dinky-web/src/pages/DataStudio/Toolbar/Resource/index.tsx @@ -0,0 +1,396 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { connect, history } from '@@/exports'; +import { DataStudioState } from '@/pages/DataStudio/model'; +import { mapDispatchToProps } from '@/pages/DataStudio/DvaFunction'; +import React, { useCallback, useEffect, useState } from 'react'; +import { ResourceState } from '@/types/RegCenter/state'; +import { InitResourceState } from '@/types/RegCenter/init.d'; +import { API_CONSTANTS } from '@/services/endpoints'; +import { + handleGetOption, + handleOption, + handleRemoveById, + queryDataByParams +} from '@/services/BusinessCrud'; +import { ResourceInfo } from '@/types/RegCenter/data'; +import { SysConfigStateType } from '@/pages/SettingCenter/GlobalSetting/model'; +import { handleCopyToClipboard, unSupportView } from '@/utils/function'; +import { + ResourceRightMenuKey, + RIGHT_CONTEXT_FILE_MENU, + RIGHT_CONTEXT_FOLDER_MENU +} from '@/pages/RegCenter/Resource/components/constants'; +import { Button, Modal, Result } from 'antd'; +import { l } from '@/utils/intl'; +import { AuthorizedObject, useAccess } from '@/hooks/useAccess'; +import { ProCard } from '@ant-design/pro-components'; +import { WarningOutlined } from '@ant-design/icons'; +import FileTree from '@/pages/RegCenter/Resource/components/FileTree'; +import RightContextMenu from '@/components/RightContextMenu'; +import ResourceModal from '@/pages/RegCenter/Resource/components/ResourceModal'; +import ResourcesUploadModal from '@/pages/RegCenter/Resource/components/ResourcesUploadModal'; +import { MenuInfo } from 'rc-menu/es/interface'; + +const Resource = (props: { + enableResource: boolean; + resourcePhysicalDelete: boolean; + resourceDataList: ResourceInfo[]; + queryResource: any; +}) => { + const { resourceDataList, enableResource, queryResource, resourcePhysicalDelete } = props; + + const [resourceState, setResourceState] = useState({ + ...InitResourceState, + treeData: resourceDataList + }); + + const [editModal, setEditModal] = useState(''); + + const [uploadValue] = useState({ + url: API_CONSTANTS.BASE_URL + API_CONSTANTS.RESOURCE_UPLOAD, + pid: '', + description: '' + }); + useEffect(() => { + setResourceState((prevState) => ({ ...prevState, treeData: resourceDataList })); + }, [resourceDataList]); + + const refreshTree = async () => { + await queryResource(); + }; + + /** + * query content by id + * @type {(id: number) => Promise} + */ + const queryContent: (id: number) => Promise = useCallback(async (id: number) => { + await queryDataByParams(API_CONSTANTS.RESOURCE_GET_CONTENT_BY_ID, { + id + }).then((res) => setResourceState((prevState) => ({ ...prevState, content: res ?? '' }))); + }, []); + + /** + * the node click event + * @param info + * @returns {Promise} + */ + const handleNodeClick = async (info: any): Promise => { + const { + node: { id, isLeaf, key, name }, + node + } = info; + setResourceState((prevState) => ({ ...prevState, selectedKeys: [key], clickedNode: node })); + if (isLeaf && !unSupportView(name)) { + await queryContent(id); + } else { + setResourceState((prevState) => ({ ...prevState, content: '' })); + } + }; + + /** + * the node right click event OF upload, + */ + const handleCreateFolder = () => { + if (resourceState.rightClickedNode) { + setEditModal(ResourceRightMenuKey.CREATE_FOLDER); + const { id } = resourceState.rightClickedNode; + setResourceState((prevState) => ({ + ...prevState, + editOpen: true, + value: { id, fileName: '', description: '' }, + contextMenuOpen: false + })); + } + }; + const handleUpload = () => { + if (resourceState.rightClickedNode) { + uploadValue.pid = resourceState.rightClickedNode.id; + // todo: upload + setResourceState((prevState) => ({ ...prevState, uploadOpen: true, contextMenuOpen: false })); + } + }; + + const realDelete = async () => { + await handleRemoveById(API_CONSTANTS.RESOURCE_REMOVE, resourceState.rightClickedNode.id); + await refreshTree(); + }; + + /** + * the node right click event OF delete, + */ + const handleDelete = async () => { + if (resourceState.rightClickedNode) { + setResourceState((prevState) => ({ ...prevState, contextMenuOpen: false })); + if (resourcePhysicalDelete) { + Modal.confirm({ + title: l('rc.resource.delete'), + content: l('rc.resource.deleteConfirm'), + onOk: async () => realDelete() + }); + } else { + await realDelete(); + } + } + }; + + /** + * the node right click event OF rename, + */ + const handleRename = () => { + if (resourceState.rightClickedNode) { + setEditModal(ResourceRightMenuKey.RENAME); + const { id, name, desc } = resourceState.rightClickedNode; + setResourceState((prevState) => ({ + ...prevState, + editOpen: true, + value: { id, fileName: name, description: desc }, + contextMenuOpen: false + })); + } + }; + + const handleMenuClick = async (node: MenuInfo) => { + const { fullInfo } = resourceState.rightClickedNode; + switch (node.key) { + case ResourceRightMenuKey.CREATE_FOLDER: + handleCreateFolder(); + break; + case ResourceRightMenuKey.UPLOAD: + handleUpload(); + break; + case ResourceRightMenuKey.DELETE: + await handleDelete(); + break; + case ResourceRightMenuKey.RENAME: + handleRename(); + break; + case ResourceRightMenuKey.COPY_TO_ADD_CUSTOM_JAR: + if (fullInfo) { + const fillValue = `ADD CUSTOMJAR 'rs:${fullInfo.fullName}';`; + await handleCopyToClipboard(fillValue); + } + break; + case ResourceRightMenuKey.COPY_TO_ADD_JAR: + if (fullInfo) { + const fillValue = `ADD JAR 'rs:${fullInfo.fullName}';`; + await handleCopyToClipboard(fillValue); + } + break; + case ResourceRightMenuKey.COPY_TO_ADD_FILE: + if (fullInfo) { + const fillValue = `ADD FILE 'rs:${fullInfo.fullName}';`; + await handleCopyToClipboard(fillValue); + } + break; + case ResourceRightMenuKey.COPY_TO_ADD_RS_PATH: + if (fullInfo) { + const fillValue = `rs:${fullInfo.fullName}`; + await handleCopyToClipboard(fillValue); + } + break; + default: + break; + } + }; + + /** + * the right click event + * @param info + */ + const handleRightClick = (info: any) => { + // Obtain the node information for right-click + const { node, event } = info; + + // Determine if the position of the right button exceeds the screen. If it exceeds the screen, set it to the maximum value of the screen offset upwards by 75 (it needs to be reasonably set according to the specific number of right button menus) + if (event.clientY + 150 > window.innerHeight) { + event.clientY = window.innerHeight - 75; + } + + setResourceState((prevState) => ({ + ...prevState, + selectedKeys: [node.key], + rightClickedNode: node, + contextMenuOpen: true, + contextMenuPosition: { + ...prevState.contextMenuPosition, + top: event.clientY + 5, + left: event.clientX + 10, + screenX: event.screenX, + screenY: event.screenY + } + })); + }; + + const handleSync = async () => { + Modal.confirm({ + title: l('rc.resource.sync'), + content: l('rc.resource.sync.confirm'), + onOk: async () => { + await handleGetOption(API_CONSTANTS.RESOURCE_SYNC_DATA, l('rc.resource.sync'), {}); + await refreshTree(); + } + }); + }; + + /** + * the rename cancel + */ + const handleModalCancel = async () => { + setResourceState((prevState) => ({ ...prevState, editOpen: false })); + await refreshTree(); + }; + + /** + * the rename ok + */ + const handleModalSubmit = async (value: Partial) => { + const { id: pid } = resourceState.rightClickedNode; + if (editModal === ResourceRightMenuKey.CREATE_FOLDER) { + await handleOption( + API_CONSTANTS.RESOURCE_CREATE_FOLDER, + l('right.menu.createFolder'), + { + ...value, + pid + }, + () => handleModalCancel() + ); + } else if (editModal === ResourceRightMenuKey.RENAME) { + await handleOption( + API_CONSTANTS.RESOURCE_RENAME, + l('right.menu.rename'), + { ...value, pid }, + () => handleModalCancel() + ); + } + }; + const handleUploadCancel = async () => { + setResourceState((prevState) => ({ ...prevState, uploadOpen: false })); + await refreshTree(); + }; + const access = useAccess(); + + const renderRightMenu = () => { + if (!resourceState.rightClickedNode.isLeaf) { + return RIGHT_CONTEXT_FOLDER_MENU.filter( + (menu) => !menu.path || !!AuthorizedObject({ path: menu.path, children: menu, access }) + ); + } + return RIGHT_CONTEXT_FILE_MENU.filter( + (menu) => !menu.path || !!AuthorizedObject({ path: menu.path, children: menu, access }) + ); + }; + + /** + * render + */ + return ( + <> + {!enableResource ? ( + + } + title={l('rc.resource.enable')} + subTitle={l('rc.resource.enable.tips')} + extra={ + + } + /> + + ) : ( + <> + + handleNodeClick(info)} + onSync={handleSync} + /> + + setResourceState((prevState) => ({ ...prevState, contextMenuOpen: false })) + } + items={renderRightMenu()} + onClick={handleMenuClick} + /> + + {resourceState.editOpen && ( + + )} + {resourceState.uploadOpen && ( + + )} + + )} + + ); +}; + +export default connect( + ({ DataStudio, SysConfig }: { DataStudio: DataStudioState; SysConfig: SysConfigStateType }) => ({ + resourceDataList: DataStudio.tempData.resourceDataList, + enableResource: SysConfig.enableResource, + resourcePhysicalDelete: SysConfig.resourcePhysicalDelete + }), + mapDispatchToProps +)(Resource); diff --git a/dinky-web/src/pages/DataStudio/Toolbar/ToolbarRoute.tsx b/dinky-web/src/pages/DataStudio/Toolbar/ToolbarRoute.tsx index f22efa3972..52a6dc29c9 100644 --- a/dinky-web/src/pages/DataStudio/Toolbar/ToolbarRoute.tsx +++ b/dinky-web/src/pages/DataStudio/Toolbar/ToolbarRoute.tsx @@ -22,6 +22,7 @@ import { CodeOutlined, ConsoleSqlOutlined, DatabaseOutlined, + FileZipOutlined, FunctionOutlined, SettingOutlined, TableOutlined, @@ -41,6 +42,7 @@ const Service = lazy(() => import('@/pages/DataStudio/Toolbar/Service')); const Tool = lazy(() => import('@/pages/DataStudio/Toolbar/Tool')); const Catalog = lazy(() => import('@/pages/DataStudio/Toolbar/Catalog')); const FlinkSqlClient = lazy(() => import('@/pages/DataStudio/Toolbar/FlinkSqlClient')); +const Resource = lazy(() => import('@/pages/DataStudio/Toolbar/Resource')); export const ToolbarRoutes: ToolbarRoute[] = [ { key: 'quick-start', @@ -97,6 +99,13 @@ export const ToolbarRoutes: ToolbarRoute[] = [ icon: , position: 'leftBottom', content: () => lazyComponent() + }, + { + key: 'resource', + title: () => l('datastudio.middle.qg.resource'), + icon: , + position: 'leftTop', + content: () => lazyComponent() } ]; diff --git a/dinky-web/src/pages/DataStudio/index.tsx b/dinky-web/src/pages/DataStudio/index.tsx index 8ef1ff4c95..d71d7ffe28 100644 --- a/dinky-web/src/pages/DataStudio/index.tsx +++ b/dinky-web/src/pages/DataStudio/index.tsx @@ -79,7 +79,8 @@ const DataStudio: React.FC = (props: any) => { queryUserData, queryDsConfig, queryTaskOwnerLockingStrategy, - queryResource + queryResource, + queryResourceConfig } = props; const [_, token] = useToken(); @@ -140,10 +141,13 @@ const DataStudio: React.FC = (props: any) => { await queryUserData({ id: getTenantByLocalStorage() }); await queryDsConfig(); await queryTaskOwnerLockingStrategy(); + await queryResourceConfig(); + }, []); + useAsyncEffect(async () => { if (enableResource) { await queryResource(); } - }, []); + }, [enableResource]); useEffect(() => { const { actionType, params } = dataStudioState.action; if (actionType?.includes('task-run-')) { diff --git a/dinky-web/src/pages/Other/Login/LoginForm/index.tsx b/dinky-web/src/pages/Other/Login/LoginForm/index.tsx index c12e470a3a..aa987a048f 100644 --- a/dinky-web/src/pages/Other/Login/LoginForm/index.tsx +++ b/dinky-web/src/pages/Other/Login/LoginForm/index.tsx @@ -17,26 +17,26 @@ * */ -import {API_CONSTANTS} from '@/services/endpoints'; -import {l} from '@/utils/intl'; -import {GithubOutlined, LockOutlined, UserOutlined} from '@ant-design/icons'; -import {DefaultFooter, ProForm, ProFormCheckbox, ProFormText} from '@ant-design/pro-components'; -import {SubmitterProps} from '@ant-design/pro-form/es/components'; -import {Col, Flex, Row} from 'antd'; -import React, {useState} from 'react'; +import { API_CONSTANTS } from '@/services/endpoints'; +import { l } from '@/utils/intl'; +import { GithubOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'; +import { DefaultFooter, ProForm, ProFormCheckbox, ProFormText } from '@ant-design/pro-components'; +import { SubmitterProps } from '@ant-design/pro-form/es/components'; +import { Col, Flex, Row } from 'antd'; +import React, { useState } from 'react'; import style from '../../../../global.less'; import Lottie from 'react-lottie'; import DataPlatform from '../../../../../public/login_animation.json'; -import {useRequest} from '@@/exports'; -import {history} from '@umijs/max'; -import {GLOBAL_SETTING_KEYS} from '@/types/SettingCenter/data.d'; +import { useRequest } from '@@/exports'; +import { history } from '@umijs/max'; +import { GLOBAL_SETTING_KEYS } from '@/types/SettingCenter/data.d'; type LoginFormProps = { onSubmit: (values: any) => Promise; }; const LoginForm: React.FC = (props) => { - const {onSubmit} = props; + const { onSubmit } = props; const [form] = ProForm.useForm(); @@ -55,7 +55,7 @@ const LoginForm: React.FC = (props) => { const handleClickLogin = async () => { setSubmitting(true); - await onSubmit({...form.getFieldsValue()}); + await onSubmit({ ...form.getFieldsValue() }); setSubmitting(false); }; @@ -66,7 +66,7 @@ const LoginForm: React.FC = (props) => { name='username' fieldProps={{ size: 'large', - prefix: + prefix: }} required placeholder={l('login.username.placeholder')} @@ -81,7 +81,7 @@ const LoginForm: React.FC = (props) => { name='password' fieldProps={{ size: 'large', - prefix: + prefix: }} placeholder={l('login.password.placeholder')} rules={[ @@ -95,7 +95,7 @@ const LoginForm: React.FC = (props) => { {l('login.rememberMe')} - + @@ -106,7 +106,7 @@ const LoginForm: React.FC = (props) => { }; const proFormSubmitter: SubmitterProps = { - searchConfig: {submitText: l('menu.login')}, + searchConfig: { submitText: l('menu.login') }, resetButtonProps: false, submitButtonProps: { loading: submitting, @@ -114,7 +114,7 @@ const LoginForm: React.FC = (props) => { htmlType: 'submit', size: 'large', shape: 'round', - style: {width: '100%'} + style: { width: '100%' } } }; @@ -140,7 +140,13 @@ const LoginForm: React.FC = (props) => { }} > = (props) => { xxl={8} > - + - {''}/ -

{l('layouts.userLayout.title')}

+ {''} +

{l('layouts.userLayout.title')}

- + {renderLoginForm()} @@ -173,7 +179,7 @@ const LoginForm: React.FC = (props) => {
= (props) => { }, { key: 'github', - title: , + title: , href: 'https://github.com/DataLinkDC/dinky', blankTarget: true } @@ -203,7 +209,7 @@ const LoginForm: React.FC = (props) => { height: '100%' }} > - + = (props) => { src={'./icons/footer-bg.svg'} width={'100%'} alt={''} - style={{position: 'absolute', bottom: 0}} + style={{ position: 'absolute', bottom: 0 }} /> ); diff --git a/dinky-web/src/pages/SettingCenter/GlobalSetting/model.ts b/dinky-web/src/pages/SettingCenter/GlobalSetting/model.ts index 8f297cb0a6..f6fb589460 100644 --- a/dinky-web/src/pages/SettingCenter/GlobalSetting/model.ts +++ b/dinky-web/src/pages/SettingCenter/GlobalSetting/model.ts @@ -101,8 +101,8 @@ const ConfigModel: ConfigModelType = { }); } }, - *queryResourceConfig({ payload }, { call, put }) { - const response: BaseConfigProperties[] = yield call(queryResourceConfig, payload); + *queryResourceConfig({}, { call, put }) { + const response: BaseConfigProperties[] = yield call(queryResourceConfig); yield put({ type: 'saveDsConfig', payload: response || [] diff --git a/dinky-web/src/pages/SettingCenter/GlobalSetting/service.ts b/dinky-web/src/pages/SettingCenter/GlobalSetting/service.ts index 679443b584..5cd0f79cb5 100644 --- a/dinky-web/src/pages/SettingCenter/GlobalSetting/service.ts +++ b/dinky-web/src/pages/SettingCenter/GlobalSetting/service.ts @@ -27,8 +27,10 @@ export async function queryDsConfig() { }); } -export async function queryResourceConfig(keyword: string) { - return await queryDataByParams(API_CONSTANTS.SYSTEM_GET_ONE_TYPE_CONFIG, { type: keyword }); +export async function queryResourceConfig() { + return await queryDataByParams(API_CONSTANTS.SYSTEM_GET_ONE_TYPE_CONFIG, { + type: SettingConfigKeyEnum.RESOURCE.toLowerCase() + }); } export async function queryTaskOwnerLockingStrategy() {