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>

Reply via email to