This is an automated email from the ASF dual-hosted git repository. rickyma pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-uniffle.git
The following commit(s) were added to refs/heads/master by this push: new 77e8612f9 [#1401] feat(dashboard): Support removing excluded servers (#1969) 77e8612f9 is described below commit 77e8612f96d10bd9d438d069081c3d7944ced11b Author: yl09099 <33595968+yl09...@users.noreply.github.com> AuthorDate: Fri Aug 2 20:32:04 2024 +0800 [#1401] feat(dashboard): Support removing excluded servers (#1969) ### What changes were proposed in this pull request? 1. Provide the page deletion function for blacklist pages. 2. Provide search function for blacklist page. ![image](https://github.com/user-attachments/assets/3e737f69-9a23-4e20-ba31-7d05c7300fdd) ![image](https://github.com/user-attachments/assets/7e3ec007-ff1f-4a52-88ac-e23f17dd56e9) ### Why are the changes needed? Fix: #1401 ### Does this PR introduce _any_ user-facing change? Yes. ### How was this patch tested? UT. --- .../apache/uniffle/coordinator/ClusterManager.java | 2 + .../uniffle/coordinator/SimpleClusterManager.java | 28 ++++ .../coordinator/web/resource/ServerResource.java | 11 ++ dashboard/src/main/webapp/src/api/api.js | 5 + dashboard/src/main/webapp/src/main.js | 2 +- .../src/pages/serverstatus/ExcludeNodeList.vue | 181 ++++++++++++++++----- 6 files changed, 190 insertions(+), 39 deletions(-) diff --git a/coordinator/src/main/java/org/apache/uniffle/coordinator/ClusterManager.java b/coordinator/src/main/java/org/apache/uniffle/coordinator/ClusterManager.java index d4a80122d..1990213b3 100644 --- a/coordinator/src/main/java/org/apache/uniffle/coordinator/ClusterManager.java +++ b/coordinator/src/main/java/org/apache/uniffle/coordinator/ClusterManager.java @@ -84,4 +84,6 @@ public interface ClusterManager extends Closeable { /** Add blacklist. */ boolean addExcludedNodes(List<String> excludedNodeIds); + + boolean removeExcludedNodesFromFile(List<String> excludedNodeIds); } diff --git a/coordinator/src/main/java/org/apache/uniffle/coordinator/SimpleClusterManager.java b/coordinator/src/main/java/org/apache/uniffle/coordinator/SimpleClusterManager.java index 3c27b8d89..1bd73dbe4 100644 --- a/coordinator/src/main/java/org/apache/uniffle/coordinator/SimpleClusterManager.java +++ b/coordinator/src/main/java/org/apache/uniffle/coordinator/SimpleClusterManager.java @@ -270,6 +270,21 @@ public class SimpleClusterManager implements ClusterManager { return true; } + private synchronized boolean removeExcludedNodesFile(List<String> excludedNodes) + throws IOException { + if (hadoopFileSystem == null) { + return false; + } + Path hadoopPath = new Path(excludedNodesPath); + // Obtains the existing excluded node. + Set<String> alreadyExistExcludedNodes = + parseExcludedNodesFile(hadoopFileSystem.open(hadoopPath)); + // Writes to the new excluded node. + alreadyExistExcludedNodes.removeAll(excludedNodes); + writeExcludedNodes2File(Lists.newArrayList(alreadyExistExcludedNodes)); + return true; + } + @Override public void add(ServerNode node) { ServerNode pre = servers.get(node.getId()); @@ -383,6 +398,19 @@ public class SimpleClusterManager implements ClusterManager { } } + @Override + public boolean removeExcludedNodesFromFile(List<String> excludedNodeIds) { + try { + if (removeExcludedNodesFile(excludedNodeIds)) { + excludedNodes.removeAll(excludedNodeIds); + return true; + } + } catch (IOException e) { + LOG.warn("Because {}, failed to add blacklist.", e.getMessage()); + } + return false; + } + @VisibleForTesting public void clear() { servers.clear(); diff --git a/coordinator/src/main/java/org/apache/uniffle/coordinator/web/resource/ServerResource.java b/coordinator/src/main/java/org/apache/uniffle/coordinator/web/resource/ServerResource.java index 8db72a19e..ade673112 100644 --- a/coordinator/src/main/java/org/apache/uniffle/coordinator/web/resource/ServerResource.java +++ b/coordinator/src/main/java/org/apache/uniffle/coordinator/web/resource/ServerResource.java @@ -233,6 +233,17 @@ public class ServerResource extends BaseResource { return Response.fail("fail"); } + @POST + @Path("/removeExcludeNodes") + @Consumes(MediaType.APPLICATION_JSON) + public Response<String> handleDeleteExcludeNodesRequest(Map<String, List<String>> excludeNodes) { + ClusterManager clusterManager = getClusterManager(); + if (clusterManager.removeExcludedNodesFromFile(excludeNodes.get("excludeNodes"))) { + return Response.success("success"); + } + return Response.fail("fail"); + } + private ClusterManager getClusterManager() { return (ClusterManager) servletContext.getAttribute(ClusterManager.class.getCanonicalName()); } diff --git a/dashboard/src/main/webapp/src/api/api.js b/dashboard/src/main/webapp/src/api/api.js index eb4755183..c0a827f44 100644 --- a/dashboard/src/main/webapp/src/api/api.js +++ b/dashboard/src/main/webapp/src/api/api.js @@ -94,6 +94,11 @@ export function addShuffleExcludeNodes(params, headers) { return http.post('/server/addExcludeNodes', params, headers, 0) } +// Create an interface for remove blacklist +export function removeShuffleExcludeNodes(params, headers) { + return http.post('/server/removeExcludeNodes', params, headers, 0) +} + // Total number of interfaces for new App export function getAppTotal(params, headers) { return http.get('/app/total', params, headers, 0) diff --git a/dashboard/src/main/webapp/src/main.js b/dashboard/src/main/webapp/src/main.js index 3a3cbdace..2d2e95d73 100644 --- a/dashboard/src/main/webapp/src/main.js +++ b/dashboard/src/main/webapp/src/main.js @@ -22,7 +22,7 @@ import ElementPlus from 'element-plus' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import 'element-plus/dist/index.css' import router from '@/router' -// import '@/mock' // With this annotation turned on, you can use the front-end mock data without requesting a background interface. +// import '@/mock' // With this annotation turned on, you can use the front-end mock data without requesting a background interface. const app = createApp(App) const pinia = createPinia() diff --git a/dashboard/src/main/webapp/src/pages/serverstatus/ExcludeNodeList.vue b/dashboard/src/main/webapp/src/pages/serverstatus/ExcludeNodeList.vue index c1b5fa0af..e12768fdd 100644 --- a/dashboard/src/main/webapp/src/pages/serverstatus/ExcludeNodeList.vue +++ b/dashboard/src/main/webapp/src/pages/serverstatus/ExcludeNodeList.vue @@ -17,19 +17,32 @@ <template> <div> - <div style="text-align: right"> - <el-button type="primary" @click="dialogFormVisible = true"> Add Node </el-button> + <div class="button-wrapper"> + <el-button type="success" @click="dialogFormVisible = true"> Add Node </el-button> + <el-button type="danger" @click="handleDeleteNode">Delete({{ selectItemNum }})</el-button> </div> + <el-divider /> <div> <el-table - :data="pageData.tableData" - height="550" - style="width: 100%" + :data="filteredTableData" + :row-key="rowKey" :default-sort="sortColumn" @sort-change="sortChangeEvent" + @selection-change="handlerSelectionChange" + class="table-wapper" + ref="table" + stripe > + <el-table-column type="selection" width="55" /> <el-table-column prop="id" label="ExcludeNodeId" min-width="180" :sortable="true" /> + <el-table-column align="right"> + <template #header> + <el-input v-model="searchKeyword" size="small" placeholder="Type to search" /> + </template> + </el-table-column> </el-table> + </div> + <div> <el-dialog v-model="dialogFormVisible" title="Please enter the server id list to be excluded:" @@ -49,7 +62,7 @@ <template #footer> <div class="dialog-footer"> <el-button @click="dialogFormVisible = false">Cancel</el-button> - <el-button type="primary" @click="confirmAddHandler"> Confirm </el-button> + <el-button type="primary" @click="handleConfirmAddHandler"> Confirm </el-button> </div> </template> </el-dialog> @@ -57,20 +70,18 @@ </div> </template> <script> -import { onMounted, reactive, ref, inject } from 'vue' -import { addShuffleExcludeNodes, getShuffleExcludeNodes } from '@/api/api' +import { onMounted, reactive, ref, inject, computed } from 'vue' +import { + addShuffleExcludeNodes, + getShuffleExcludeNodes, + removeShuffleExcludeNodes +} from '@/api/api' import { useCurrentServerStore } from '@/store/useCurrentServerStore' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' export default { setup() { - const pageData = reactive({ - tableData: [ - { - id: '' - } - ] - }) + const pageData = reactive({ tableData: [] }) const currentServerStore = useCurrentServerStore() const dialogFormVisible = ref(false) @@ -87,25 +98,6 @@ export default { pageData.tableData = res.data.data } - async function addShuffleExcludeNodesPage() { - try { - const excludeNodes = textarea.value.split('\n').map((item) => item.trim()) - const excludeNodesObj = { excludeNodes } - const res = await addShuffleExcludeNodes(excludeNodesObj) - if (res.status >= 200 && res.status < 300) { - if (res.data.data === 'success') { - ElMessage.success('Add successfully.') - } else { - ElMessage.error('Add failed.') - } - } else { - ElMessage.error('Failed to add due to server bad.') - } - } catch (err) { - ElMessage.error('Failed to add due to network exception.') - } - } - // The system obtains data from global variables and requests the interface to obtain new data after data changes. currentServerStore.$subscribe((mutable, state) => { if (state.currentServer) { @@ -120,6 +112,9 @@ export default { } }) + /** + * The following describes how to handle sort events. + */ const sortColumn = reactive({}) const sortChangeEvent = (sortInfo) => { for (const sortColumnKey in sortColumn) { @@ -127,7 +122,11 @@ export default { } sortColumn[sortInfo.prop] = sortInfo.order } - const confirmAddHandler = () => { + + /** + * The following describes how to handle add events. + */ + function handleConfirmAddHandler() { dialogFormVisible.value = false addShuffleExcludeNodesPage() // Refreshing the number of blacklists. @@ -136,14 +135,115 @@ export default { getShuffleExcludeNodesPage() } + async function addShuffleExcludeNodesPage() { + try { + const excludeNodes = textarea.value.split('\n').map((item) => item.trim()) + const excludeNodesObj = { excludeNodes } + const res = await addShuffleExcludeNodes(excludeNodesObj) + if (res.status >= 200 && res.status < 300) { + if (res.data.data === 'success') { + ElMessage.success('Add successfully.') + } else { + ElMessage.error('Add failed.') + } + } else { + ElMessage.error('Failed to add due to server bad.') + } + } catch (err) { + ElMessage.error('Failed to add due to network exception.') + } + } + + /** + * The following describes how to handle blacklist deletion events. + */ + const selectItemNum = ref(0) + const rowKey = 'id' + const selectedRows = ref([]) + function handlerSelectionChange(selection) { + selectedRows.value = selection + selectItemNum.value = selectedRows.value.length + } + function handleDeleteNode() { + ElMessageBox.confirm('Are you sure about removing these nodes?', 'Warning', { + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + type: 'warning' + }) + .then(() => { + if (selectedRows.value.length === 0) { + ElMessage({ + type: 'info', + message: 'No node is selected, Nothing!' + }) + } else { + const selectedIds = selectedRows.value.map((row) => row[rowKey]) + // pageData.tableData = pageData.tableData.filter(row => !selectedIds.includes(row[rowKey])); + deleteShuffleExcludedNodes(selectedIds) + // Refreshing the number of blacklists. + updateTotalPage() + // Refreshing the Blacklist list. + getShuffleExcludeNodesPage() + ElMessage({ + type: 'success', + message: 'Delete completed' + }) + } + }) + .catch(() => { + ElMessage({ + type: 'info', + message: 'Delete canceled' + }) + }) + } + + async function deleteShuffleExcludedNodes(excludeNodes) { + try { + const excludeNodesObj = { excludeNodes } + const res = await removeShuffleExcludeNodes(excludeNodesObj) + if (res.status >= 200 && res.status < 300) { + if (res.data.data === 'success') { + ElMessage.success('Add successfully.') + } else { + ElMessage.error('Add failed.') + } + } else { + ElMessage.error('Failed to add due to server bad.') + } + } catch (err) { + ElMessage.error('Failed to add due to network exception.') + } + } + + /** + * The following describes how to handle blacklist select events. + */ + const searchKeyword = ref('') + const filteredTableData = computed(() => { + const keyword = searchKeyword.value.trim() + if (!keyword) { + return pageData.tableData + } else { + return pageData.tableData.filter((row) => { + return row.id.includes(keyword) + }) + } + }) return { pageData, sortColumn, + selectItemNum, sortChangeEvent, - confirmAddHandler, + handleConfirmAddHandler, + handleDeleteNode, + handlerSelectionChange, dialogFormVisible, formLabelWidth, - textarea + textarea, + rowKey, + searchKeyword, + filteredTableData } } } @@ -156,4 +256,9 @@ export default { .dialog-wrapper { width: 50%; } +.table-wapper { + height: 550px; + width: 100%; + text-align: right; +} </style>