Forráskód Böngészése

添加了管理员全局搜索Value值的功能 (#5182)

* 添加了管理员全局搜索Value值的功能

* Update ItemControllerTest.java

* Update GlobalSearchValueController.js

* Improved some project codes

* Optimized some issues

* Added some relevant documents

* Update docs/zh/portal/apollo-user-guide.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update docs/zh/portal/apollo-user-guide.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update README.md

* Added usage documentation

* Update docs/zh/README.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fixed some issues

* Fixed the front-end

* Update GlobalSearchValueController.js

* Optimized some of the logic

* Added some unit tests

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
xiaoxianhjy 6 hónapja
szülő
commit
7e7b0902f0
43 módosított fájl, 1581 hozzáadás és 56 törlés
  1. 2 2
      CHANGES.md
  2. 3 8
      README.md
  3. 9 0
      apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java
  4. 45 20
      apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java
  5. 17 0
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java
  6. 14 4
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java
  7. 25 0
      apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java
  8. 88 0
      apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTO.java
  9. 67 0
      apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/SearchResponseEntity.java
  10. 61 0
      apollo-common/src/test/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTOTest.java
  11. 62 0
      apollo-common/src/test/java/com/ctrip/framework/apollo/common/http/SearchResponseEntityTest.java
  12. 12 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java
  13. 2 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java
  14. 54 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchController.java
  15. 100 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemInfo.java
  16. 77 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/GlobalSearchService.java
  17. 196 0
      apollo-portal/src/main/resources/static/global_search_value.html
  18. 24 1
      apollo-portal/src/main/resources/static/i18n/en.json
  19. 24 1
      apollo-portal/src/main/resources/static/i18n/zh-CN.json
  20. BIN
      apollo-portal/src/main/resources/static/img/nodata.png
  21. 2 0
      apollo-portal/src/main/resources/static/scripts/app.js
  22. 273 0
      apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js
  23. 40 0
      apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js
  24. 1 0
      apollo-portal/src/main/resources/static/views/common/nav.html
  25. 135 0
      apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java
  26. 127 0
      apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java
  27. 13 0
      changes/changes-2.4.0.md
  28. 4 0
      docs/en/README.md
  29. 11 3
      docs/en/deployment/distributed-deployment-guide.md
  30. 1 1
      docs/en/design/apollo-design.md
  31. 6 0
      docs/en/design/apollo-introduction.md
  32. BIN
      docs/en/images/Configuration query-Non properties.png
  33. BIN
      docs/en/images/Configuration query-properties.png
  34. BIN
      docs/en/images/System-parameterization-of-global-search-configuration-items.png
  35. 27 0
      docs/en/portal/apollo-user-guide.md
  36. 4 0
      docs/zh/README.md
  37. 8 0
      docs/zh/deployment/distributed-deployment-guide.md
  38. 1 1
      docs/zh/design/apollo-design.md
  39. 15 11
      docs/zh/design/apollo-introduction.md
  40. BIN
      docs/zh/images/Configuration query-Non properties.png
  41. BIN
      docs/zh/images/Configuration query-properties.png
  42. BIN
      docs/zh/images/System-parameterization-of-global-search-configuration-items.png
  43. 31 4
      docs/zh/portal/apollo-user-guide.md

+ 2 - 2
CHANGES.md

@@ -7,7 +7,7 @@ Apollo 2.4.0
 ------------------
 * [Update the server config link in system info page](https://github.com/apolloconfig/apollo/pull/5204)
 * [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200)
-
+* [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182)
 
 ------------------
-All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)
+All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)

+ 3 - 8
README.md

@@ -37,36 +37,31 @@ Demo Environment:
   * The same codebase could have different configurations when deployed in different clusters
   * With the namespace concept, it is easy to support multiple applications to share the same configurations, while also allowing them to customize the configurations
   * Multiple languages is provided in user interface(currently Chinese and English)
-
 * **Configuration changes takes effect in real time (hot release)**
   * After the user modified the configuration and released it in Apollo, the sdk will receive the latest configurations in real time (1 second) and notify the application
-
 * **Release version management**
   * Every configuration releases are versioned, which is friendly to support configuration rollback
-
 * **Grayscale release**
   * Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem
-
+* **Global Search Configuration Items**
+  * A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used
+  * It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations
 * **Authorization management, release approval and operation audit**
   * Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors
   * All operations have audit logs for easy tracking of problems
-
 * **Client side configuration information monitoring**
   * It's very easy to see which instances are using the configurations and what versions they are using
-
 * **Rich SDKs available**
   * Provides native sdks of Java and .Net to facilitate application integration
   * Support Spring Placeholder, Annotation and Spring Boot ConfigurationProperties for easy application use (requires Spring 3.1.1+)
   * Http APIs are provided, so non-Java and .Net applications can integrate conveniently
   * Rich third party sdks are also available, e.g. Golang, Python, NodeJS, PHP, C, etc
-
 * **Open platform API**
   * Apollo itself provides a unified configuration management interface, which supports features such as multi-environment, multi-data center configuration management, permissions, and process governance
   * However, for the sake of versatility, Apollo will not put too many restrictions on the modification of the configuration, as long as it conforms to the basic format, it can be saved.
   * In our research, we found that for some users, their configurations may have more complicated formats, such as xml, json, and the format needs to be verified
   * There are also some users such as DAL, which not only have a specific format, but also need to verify the entered value before saving, such as checking whether the database, username and password match
   * For this type of application, Apollo allows the application to modify and release configurations through open APIs, which has great authorization and permission control mechanism built in
-
 * **Simple deployment**
   * As an infrastructure service, the configuration center has very high availability requirements, which forces Apollo to rely on external dependencies as little as possible
   * Currently, the only external dependency is MySQL, so the deployment is very simple. Apollo can run as long as Java and MySQL are installed

+ 9 - 0
apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java

@@ -27,6 +27,7 @@ import com.ctrip.framework.apollo.biz.service.NamespaceService;
 import com.ctrip.framework.apollo.biz.service.ReleaseService;
 import com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder;
 import com.ctrip.framework.apollo.common.dto.ItemDTO;
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
 import com.ctrip.framework.apollo.common.dto.PageDTO;
 import com.ctrip.framework.apollo.common.exception.BadRequestException;
 import com.ctrip.framework.apollo.common.exception.NotFoundException;
@@ -201,6 +202,14 @@ public class ItemController {
     return Collections.emptyList();
   }
 
+  @GetMapping("/items-search/key-and-value")
+  public PageDTO<ItemInfoDTO> getItemInfoBySearch(@RequestParam(value = "key", required = false) String key,
+                                                  @RequestParam(value = "value", required = false) String value,
+                                                  Pageable limit) {
+    Page<ItemInfoDTO> pageItemInfoDTO = itemService.getItemInfoBySearch(key, value, limit);
+    return new PageDTO<>(pageItemInfoDTO.getContent(), limit, pageItemInfoDTO.getTotalElements());
+  }
+
   @GetMapping("/items/{itemId}")
   public ItemDTO get(@PathVariable("itemId") long itemId) {
     Item item = itemService.findOne(itemId);

+ 45 - 20
apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java

@@ -21,18 +21,19 @@ import static org.assertj.core.api.Assertions.assertThat;
 import com.ctrip.framework.apollo.biz.entity.Commit;
 import com.ctrip.framework.apollo.biz.repository.CommitRepository;
 import com.ctrip.framework.apollo.biz.repository.ItemRepository;
-import com.ctrip.framework.apollo.common.dto.AppDTO;
-import com.ctrip.framework.apollo.common.dto.ClusterDTO;
-import com.ctrip.framework.apollo.common.dto.ItemDTO;
-import com.ctrip.framework.apollo.common.dto.NamespaceDTO;
+import com.ctrip.framework.apollo.biz.service.ItemService;
+import com.ctrip.framework.apollo.common.dto.*;
+
 import java.util.List;
 import java.util.Objects;
 import org.junit.Assert;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.*;
 import org.springframework.test.context.jdbc.Sql;
 import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
 
@@ -48,6 +49,9 @@ public class ItemControllerTest extends AbstractControllerTest {
   @Autowired
   private ItemRepository itemRepository;
 
+  @Autowired
+  private ItemService itemService;
+
   @Test
   @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
   @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
@@ -58,7 +62,7 @@ public class ItemControllerTest extends AbstractControllerTest {
     ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default");
     assert cluster != null;
     NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(),
-        NamespaceDTO.class, app.getAppId(), cluster.getName(), "application");
+            NamespaceDTO.class, app.getAppId(), cluster.getName(), "application");
 
     String itemKey = "test-key";
     String itemValue = "test-value";
@@ -68,12 +72,12 @@ public class ItemControllerTest extends AbstractControllerTest {
     item.setDataChangeLastModifiedBy("apollo");
 
     ResponseEntity<ItemDTO> response = restTemplate.postForEntity(itemBaseUrl(),
-        item, ItemDTO.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName());
+            item, ItemDTO.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName());
     Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
     Assert.assertEquals(itemKey, Objects.requireNonNull(response.getBody()).getKey());
 
     List<Commit> commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(),
-        Pageable.ofSize(10));
+            Pageable.ofSize(10));
     Assert.assertEquals(1, commitList.size());
 
     Commit commit = commitList.get(0);
@@ -93,15 +97,15 @@ public class ItemControllerTest extends AbstractControllerTest {
     ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default");
     assert cluster != null;
     NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(),
-        NamespaceDTO.class, app.getAppId(), cluster.getName(), "application");
+            NamespaceDTO.class, app.getAppId(), cluster.getName(), "application");
 
     String itemKey = "test-key";
     String itemValue = "test-value-updated";
 
     long itemId = itemRepository.findByKey(itemKey, Pageable.ofSize(1))
-        .getContent()
-        .get(0)
-        .getId();
+            .getContent()
+            .get(0)
+            .getId();
     ItemDTO item = new ItemDTO(itemKey, itemValue, "", 1);
     item.setDataChangeLastModifiedBy("apollo");
 
@@ -115,7 +119,7 @@ public class ItemControllerTest extends AbstractControllerTest {
     });
 
     List<Commit> commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(),
-        Pageable.ofSize(10));
+            Pageable.ofSize(10));
     assertThat(commitList).hasSize(2);
   }
 
@@ -131,23 +135,44 @@ public class ItemControllerTest extends AbstractControllerTest {
     ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default");
     assert cluster != null;
     NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(),
-        NamespaceDTO.class, app.getAppId(), cluster.getName(), "application");
+            NamespaceDTO.class, app.getAppId(), cluster.getName(), "application");
 
     String itemKey = "test-key";
 
     long itemId = itemRepository.findByKey(itemKey, Pageable.ofSize(1))
-        .getContent()
-        .get(0)
-        .getId();
+            .getContent()
+            .get(0)
+            .getId();
 
     String deleteUrl = url(  "/items/{itemId}?operator=apollo");
     restTemplate.delete(deleteUrl, itemId);
     assertThat(itemRepository.findById(itemId).isPresent())
-        .isFalse();
+            .isFalse();
 
     assert namespace != null;
     List<Commit> commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(),
-        Pageable.ofSize(10));
+            Pageable.ofSize(10));
     assertThat(commitList).hasSize(2);
   }
+
+  @Test
+  @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
+  public void testSearch() {
+    this.testCreate();
+
+    String itemKey = "test-key";
+    String itemValue = "test-value";
+    Page<ItemInfoDTO> itemInfoDTOS = itemService.getItemInfoBySearch(itemKey, itemValue, PageRequest.of(0, 200));
+    HttpHeaders headers = new HttpHeaders();
+    HttpEntity<Void> entity = new HttpEntity<>(headers);
+    ResponseEntity<PageDTO<ItemInfoDTO>> response = restTemplate.exchange(
+            url("/items-search/key-and-value?key={key}&value={value}&page={page}&size={size}"),
+            HttpMethod.GET,
+            entity,
+            new ParameterizedTypeReference<PageDTO<ItemInfoDTO>>() {},
+            itemKey, itemValue, 0, 200
+    );
+    assertThat(itemInfoDTOS.getContent().toString()).isEqualTo(response.getBody().getContent().toString());
+  }
 }

+ 17 - 0
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java

@@ -18,11 +18,13 @@ package com.ctrip.framework.apollo.biz.repository;
 
 import com.ctrip.framework.apollo.biz.entity.Item;
 
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.PagingAndSortingRepository;
+import org.springframework.data.repository.query.Param;
 
 import java.util.Date;
 import java.util.List;
@@ -43,6 +45,21 @@ public interface ItemRepository extends PagingAndSortingRepository<Item, Long> {
   
   Item findFirst1ByNamespaceIdOrderByLineNumDesc(Long namespaceId);
 
+  @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " +
+          "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " +
+          "WHERE i.key LIKE %:key% AND i.value LIKE %:value% AND i.isDeleted = 0")
+  Page<ItemInfoDTO> findItemsByKeyAndValueLike(@Param("key") String key, @Param("value") String value, Pageable pageable);
+
+  @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " +
+          "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " +
+          "WHERE i.key LIKE %:key% AND i.isDeleted = 0")
+  Page<ItemInfoDTO> findItemsByKeyLike(@Param("key") String key, Pageable pageable);
+
+  @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " +
+          "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " +
+          "WHERE i.value LIKE %:value% AND i.isDeleted = 0")
+  Page<ItemInfoDTO> findItemsByValueLike(@Param("value") String value, Pageable pageable);
+
   @Modifying
   @Query("update Item set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000), DataChange_LastModifiedBy = ?2 where NamespaceId = ?1 and IsDeleted = false")
   int deleteByNamespaceId(long namespaceId, String operator);

+ 14 - 4
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java

@@ -22,6 +22,7 @@ import com.ctrip.framework.apollo.biz.entity.Audit;
 import com.ctrip.framework.apollo.biz.entity.Item;
 import com.ctrip.framework.apollo.biz.entity.Namespace;
 import com.ctrip.framework.apollo.biz.repository.ItemRepository;
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
 import com.ctrip.framework.apollo.common.exception.BadRequestException;
 import com.ctrip.framework.apollo.common.exception.NotFoundException;
 import com.ctrip.framework.apollo.common.utils.BeanUtils;
@@ -33,10 +34,7 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -146,6 +144,18 @@ public class ItemService {
     return itemRepository.findByNamespaceId(namespace.getId(), pageable);
   }
 
+  public Page<ItemInfoDTO> getItemInfoBySearch(String key, String value, Pageable limit) {
+    Page<ItemInfoDTO> itemInfoDTOs;
+    if (key.isEmpty() && !value.isEmpty()) {
+      itemInfoDTOs = itemRepository.findItemsByValueLike(value, limit);
+    } else if (value.isEmpty() && !key.isEmpty()) {
+      itemInfoDTOs = itemRepository.findItemsByKeyLike(key, limit);
+    } else {
+      itemInfoDTOs = itemRepository.findItemsByKeyAndValueLike(key, value, limit);
+    }
+    return itemInfoDTOs;
+  }
+
   @Transactional
   public Item save(Item entity) {
     checkItemKeyLength(entity.getKey());

+ 25 - 0
apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java

@@ -18,10 +18,13 @@ package com.ctrip.framework.apollo.biz.service;
 
 import com.ctrip.framework.apollo.biz.AbstractIntegrationTest;
 import com.ctrip.framework.apollo.biz.entity.Item;
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
 import com.ctrip.framework.apollo.common.exception.BadRequestException;
 import org.junit.Assert;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.test.context.jdbc.Sql;
 
 public class ItemServiceTest extends AbstractIntegrationTest {
@@ -71,4 +74,26 @@ public class ItemServiceTest extends AbstractIntegrationTest {
         Assert.assertEquals("v1-new", dbItem.getValue());
     }
 
+    @Test
+    @Sql(scripts = {"/sql/namespace-test.sql","/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+    @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+    public void testSearchItem() {
+        ItemInfoDTO itemInfoDTO = new ItemInfoDTO();
+        itemInfoDTO.setAppId("testApp");
+        itemInfoDTO.setClusterName("default");
+        itemInfoDTO.setNamespaceName("application");
+        itemInfoDTO.setKey("k1");
+        itemInfoDTO.setValue("v1");
+
+        String itemKey = "k1";
+        String itemValue = "v1";
+        Page<ItemInfoDTO> ExpectedItemInfoDTOSByKeyAndValue = itemService.getItemInfoBySearch(itemKey, itemValue, PageRequest.of(0,200));
+        Page<ItemInfoDTO> ExpectedItemInfoDTOSByKey = itemService.getItemInfoBySearch(itemKey,"", PageRequest.of(0,200));
+        Page<ItemInfoDTO> ExpectedItemInfoDTOSByValue = itemService.getItemInfoBySearch("", itemValue, PageRequest.of(0,200));
+        Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByKeyAndValue.getContent().get(0).toString());
+        Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByKey.getContent().get(0).toString());
+        Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByValue.getContent().get(0).toString());
+
+    }
+
 }

+ 88 - 0
apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTO.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.common.dto;
+
+
+public class ItemInfoDTO extends BaseDTO{
+    private String appId;
+    private String clusterName;
+    private String namespaceName;
+    private String key;
+    private String value;
+
+    public ItemInfoDTO() {
+    }
+
+    public ItemInfoDTO(String appId, String clusterName, String namespaceName, String key, String value) {
+        this.appId = appId;
+        this.clusterName = clusterName;
+        this.namespaceName = namespaceName;
+        this.key = key;
+        this.value = value;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getClusterName() {
+        return clusterName;
+    }
+
+    public void setClusterName(String clusterName) {
+        this.clusterName = clusterName;
+    }
+
+    public String getNamespaceName() {
+        return namespaceName;
+    }
+
+    public void setNamespaceName(String namespaceName) {
+        this.namespaceName = namespaceName;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "ItemInfoDTO{" +
+                "appId='" + appId + '\'' +
+                ", clusterName='" + clusterName + '\'' +
+                ", namespaceName='" + namespaceName + '\'' +
+                ", key='" + key + '\'' +
+                ", value='" + value + '\'' +
+                '}';
+    }
+}

+ 67 - 0
apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/SearchResponseEntity.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.common.http;
+
+import org.springframework.http.HttpStatus;
+
+public class SearchResponseEntity<T> {
+
+    private T body;
+    private boolean hasMoreData;
+    private Object message;
+    private int code;
+
+    public static <T> SearchResponseEntity<T> ok(T body){
+        SearchResponseEntity<T> SearchResponseEntity = new SearchResponseEntity<>();
+        SearchResponseEntity.message = HttpStatus.OK.getReasonPhrase();
+        SearchResponseEntity.code = HttpStatus.OK.value();
+        SearchResponseEntity.body = body;
+        SearchResponseEntity.hasMoreData = false;
+        return SearchResponseEntity;
+    }
+
+    public static <T> SearchResponseEntity<T> okWithMessage(T body, Object message){
+        SearchResponseEntity<T> SearchResponseEntity = new SearchResponseEntity<>();
+        SearchResponseEntity.message = message;
+        SearchResponseEntity.code = HttpStatus.OK.value();
+        SearchResponseEntity.body = body;
+        SearchResponseEntity.hasMoreData = true;
+        return SearchResponseEntity;
+    }
+
+    public static <T> SearchResponseEntity<T> error(HttpStatus httpCode, Object message){
+        SearchResponseEntity<T> SearchResponseEntity = new SearchResponseEntity<>();
+        SearchResponseEntity.message = message;
+        SearchResponseEntity.code = httpCode.value();
+        return SearchResponseEntity;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public Object getMessage() {
+        return message;
+    }
+
+    public T getBody() {
+        return body;
+    }
+
+    public boolean isHasMoreData() {return hasMoreData;}
+
+}

+ 61 - 0
apollo-common/src/test/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTOTest.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.common.dto;
+
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class ItemInfoDTOTest {
+
+    private ItemInfoDTO itemInfoDTO;
+
+    @Before
+    public void setUp() {
+        itemInfoDTO = new ItemInfoDTO("testAppId", "testClusterName", "testNamespaceName", "testKey", "testValue");
+    }
+
+    @Test
+    public void testGetAppId_ShouldReturnCorrectAppId() {
+        assertEquals("testAppId", itemInfoDTO.getAppId());
+    }
+
+    @Test
+    public void testGetClusterName_ShouldReturnCorrectClusterName() {
+        assertEquals("testClusterName", itemInfoDTO.getClusterName());
+    }
+
+    @Test
+    public void testGetNamespaceName_ShouldReturnCorrectNamespaceName() {
+        assertEquals("testNamespaceName", itemInfoDTO.getNamespaceName());
+    }
+
+    @Test
+    public void testGetKey_ShouldReturnCorrectKey() {
+        assertEquals("testKey", itemInfoDTO.getKey());
+    }
+
+    @Test
+    public void testGetValue_ShouldReturnCorrectValue() {
+        assertEquals("testValue", itemInfoDTO.getValue());
+    }
+
+    @Test
+    public void testToString_ShouldReturnExpectedString() {
+        assertEquals("ItemInfoDTO{appId='testAppId', clusterName='testClusterName', namespaceName='testNamespaceName', key='testKey', value='testValue'}", itemInfoDTO.toString());
+    }
+}

+ 62 - 0
apollo-common/src/test/java/com/ctrip/framework/apollo/common/http/SearchResponseEntityTest.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.common.http;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.HttpStatus;
+
+import static org.junit.Assert.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SearchResponseEntityTest {
+
+    @Test
+    public void testOk_WithValidBody_ShouldReturnOkResponse() {
+        String body = "test body";
+        SearchResponseEntity<String> response = SearchResponseEntity.ok(body);
+
+        assertEquals(HttpStatus.OK.value(), response.getCode());
+        assertEquals(HttpStatus.OK.getReasonPhrase(), response.getMessage());
+        assertEquals(body, response.getBody());
+        assertFalse(response.isHasMoreData());
+    }
+
+    @Test
+    public void testOkWithMessage_WithValidBodyAndMessage_ShouldReturnOkResponseWithMessage() {
+        String body = "test body";
+        String message = "test message";
+        SearchResponseEntity<String> response = SearchResponseEntity.okWithMessage(body, message);
+
+        assertEquals(HttpStatus.OK.value(), response.getCode());
+        assertEquals(message, response.getMessage());
+        assertEquals(body, response.getBody());
+        assertTrue(response.isHasMoreData());
+    }
+
+    @Test
+    public void testError_WithValidCodeAndMessage_ShouldReturnErrorResponse() {
+        HttpStatus httpCode = HttpStatus.BAD_REQUEST;
+        String message = "error message";
+        SearchResponseEntity<Object> response = SearchResponseEntity.error(httpCode, message);
+
+        assertEquals(httpCode.value(), response.getCode());
+        assertEquals(message, response.getMessage());
+        assertEquals(null, response.getBody());
+        assertFalse(response.isHasMoreData());
+    }
+}

+ 12 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java

@@ -184,6 +184,9 @@ public class AdminServiceAPI {
     private final ParameterizedTypeReference<PageDTO<OpenItemDTO>> openItemPageDTO =
             new ParameterizedTypeReference<PageDTO<OpenItemDTO>>() {};
 
+    private final ParameterizedTypeReference<PageDTO<ItemInfoDTO>> pageItemInfoDTO =
+            new ParameterizedTypeReference<PageDTO<ItemInfoDTO>>() {};
+
     public List<ItemDTO> findItems(String appId, Env env, String clusterName, String namespaceName) {
       ItemDTO[] itemDTOs =
           restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items",
@@ -198,6 +201,15 @@ public class AdminServiceAPI {
       return Arrays.asList(itemDTOs);
     }
 
+    public PageDTO<ItemInfoDTO> getPerEnvItemInfoBySearch(Env env, String key, String value, int page, int size){
+      ResponseEntity<PageDTO<ItemInfoDTO>>
+              entity =
+              restTemplate.get(env,
+                      "items-search/key-and-value?key={key}&value={value}&page={page}&size={size}",
+                      pageItemInfoDTO, key, value, page, size);
+      return entity.getBody();
+    }
+
     public ItemDTO loadItem(Env env, String appId, String clusterName, String namespaceName, String key) {
       return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}",
           ItemDTO.class, appId, clusterName, namespaceName, key);

+ 2 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java

@@ -86,6 +86,8 @@ public class PortalConfig extends RefreshableConfig {
     return envs;
   }
 
+  public int getPerEnvSearchMaxResults() {return getIntProperty("apollo.portal.search.perEnvMaxResults", 200);}
+
   /**
    * @return the relationship between environment and its meta server. empty if meet exception
    */

+ 54 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchController.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.portal.controller;
+
+
+import com.ctrip.framework.apollo.common.exception.BadRequestException;
+import com.ctrip.framework.apollo.common.http.SearchResponseEntity;
+import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
+import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo;
+import com.ctrip.framework.apollo.portal.service.GlobalSearchService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+public class GlobalSearchController {
+    private final GlobalSearchService globalSearchService;
+    private final PortalConfig portalConfig;
+
+    public GlobalSearchController(final GlobalSearchService globalSearchService, final PortalConfig portalConfig) {
+        this.globalSearchService = globalSearchService;
+        this.portalConfig = portalConfig;
+    }
+
+    @PreAuthorize(value = "@permissionValidator.isSuperAdmin()")
+    @GetMapping("/global-search/item-info/by-key-or-value")
+    public SearchResponseEntity<List<ItemInfo>> getItemInfoBySearch(@RequestParam(value = "key", required = false, defaultValue = "") String key,
+                                                                    @RequestParam(value = "value", required = false , defaultValue = "") String value) {
+
+        if(key.isEmpty() && value.isEmpty()) {
+            throw new BadRequestException("Please enter at least one search criterion in either key or value.");
+        }
+
+        return globalSearchService.getAllEnvItemInfoBySearch(key, value, 0, portalConfig.getPerEnvSearchMaxResults());
+    }
+
+}

+ 100 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemInfo.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.portal.entity.vo;
+
+public class ItemInfo {
+
+    private String appId;
+    private String envName;
+    private String clusterName;
+    private String namespaceName;
+    private String key;
+    private String value;
+
+    public ItemInfo() {
+    }
+
+    public ItemInfo(String appId, String envName, String clusterName,
+                    String namespaceName, String key, String value) {
+        this.appId = appId;
+        this.envName = envName;
+        this.clusterName = clusterName;
+        this.namespaceName = namespaceName;
+        this.key = key;
+        this.value = value;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getEnvName() {
+        return envName;
+    }
+
+    public void setEnvName(String envName) {
+        this.envName = envName;
+    }
+
+    public String getClusterName() {
+        return clusterName;
+    }
+
+    public void setClusterName(String clusterName) {
+        this.clusterName = clusterName;
+    }
+
+    public String getNamespaceName() {
+        return namespaceName;
+    }
+
+    public void setNamespaceName(String namespaceName) {
+        this.namespaceName = namespaceName;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "ItemInfo{" +
+                "appId='" + appId + '\'' +
+                ", envName='" + envName + '\'' +
+                ", clusterName='" + clusterName + '\'' +
+                ", namespaceName='" + namespaceName + '\'' +
+                ", key='" + key + '\'' +
+                ", value='" + value + '\'' +
+                '}';
+    }
+}

+ 77 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/GlobalSearchService.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.portal.service;
+
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
+import com.ctrip.framework.apollo.common.dto.PageDTO;
+import com.ctrip.framework.apollo.common.http.SearchResponseEntity;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
+import com.ctrip.framework.apollo.portal.component.PortalSettings;
+import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo;
+import com.ctrip.framework.apollo.portal.environment.Env;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Service
+public class GlobalSearchService {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalSearchService.class);
+    private final AdminServiceAPI.ItemAPI itemAPI;
+    private final PortalSettings portalSettings;
+
+    public GlobalSearchService(AdminServiceAPI.ItemAPI itemAPI, PortalSettings portalSettings) {
+        this.itemAPI = itemAPI;
+        this.portalSettings = portalSettings;
+    }
+
+    public SearchResponseEntity<List<ItemInfo>> getAllEnvItemInfoBySearch(String key, String value, int page, int size) {
+        List<Env> activeEnvs = portalSettings.getActiveEnvs();
+        List<String> envBeyondLimit = new ArrayList<>();
+        AtomicBoolean hasMoreData = new AtomicBoolean(false);
+        List<ItemInfo> allEnvItemInfos = new ArrayList<>();
+        activeEnvs.forEach(env -> {
+            PageDTO<ItemInfoDTO> perEnvItemInfoDTOs = itemAPI.getPerEnvItemInfoBySearch(env, key, value, page, size);
+            if (!perEnvItemInfoDTOs.hasContent()) {
+                return;
+            }
+            perEnvItemInfoDTOs.getContent().forEach(itemInfoDTO -> {
+                try {
+                    ItemInfo itemInfo = new ItemInfo(itemInfoDTO.getAppId(),env.getName(),itemInfoDTO.getClusterName(),itemInfoDTO.getNamespaceName(),itemInfoDTO.getKey(),itemInfoDTO.getValue());
+                    allEnvItemInfos.add(itemInfo);
+                } catch (Exception e) {
+                    LOGGER.error("Error converting ItemInfoDTO to ItemInfo for item: {}", itemInfoDTO, e);
+                }
+            });
+            if(perEnvItemInfoDTOs.getTotal() > size){
+                envBeyondLimit.add(env.getName());
+                hasMoreData.set(true);
+            }
+        });
+        if(hasMoreData.get()){
+            return SearchResponseEntity.okWithMessage(allEnvItemInfos,String.format(
+                    "In %s , more than %d items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.",
+                    String.join(" , ", envBeyondLimit), size));
+        }
+        return SearchResponseEntity.ok(allEnvItemInfos);
+    }
+
+}

+ 196 - 0
apollo-portal/src/main/resources/static/global_search_value.html

@@ -0,0 +1,196 @@
+<!--
+  ~ Copyright 2024 Apollo Authors
+  ~
+  ~ Licensed 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.
+  ~
+-->
+<!doctype html>
+<html ng-app="global_search_value">
+
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <link rel="icon" href="./img/config.png">
+    <!-- styles -->
+    <link rel="stylesheet" type="text/css" href="vendor/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" type="text/css" href="vendor/angular/angular-toastr-1.4.1.min.css">
+    <link rel="stylesheet" type="text/css" href="vendor/select2/select2.min.css">
+    <link rel="stylesheet" type="text/css" media='all' href="vendor/angular/loading-bar.min.css">
+    <link rel="stylesheet" type="text/css" href="styles/common-style.css">
+    <title>{{'Global.Title' | translate }}</title>
+</head>
+
+<body>
+<apollonav></apollonav>
+<div class="container-fluid apollo-container" ng-controller="GlobalSearchValueController">
+    <div class="col-md-10 col-md-offset-1 panel">
+        <div class="tab-content">
+            <div class="tab-pane fade in active" id="value_search" style="background-color:#fff">
+                <section class="panel-body">
+                    <div class="row">
+                        <div>
+                            <header class="panel-heading">
+                                {{'Global.Title' | translate }}
+                                <small>{{'Global.ValueSearch.Tips' | translate }}</small>
+                            </header>
+                        </div>
+                        <div style="height: 15px"></div>
+                        <div class="row">
+                            <div class="col-md-10">
+                                <form class="form-inline" style="display:inline">
+                                    <label for="searchUserInputKey" class="form-label">{{'Global.Key' | translate}}{{ ':' }}{{ '&nbsp;' }}</label>
+                                    <input type="text" class="form-control" id="searchUserInputKey"
+                                           placeholder="{{'Item.GlobalSearchByKey' | translate }}"
+                                           ng-model="itemInfoSearchKey"
+                                           style="margin-right: 10px;"
+                                           autocomplete="off"/>
+                                    <label for="searchUserInputValue" class="form-label">{{'Global.Value' | translate}}{{ ':' }}{{ '&nbsp;' }}</label>
+                                    <input type="text" class="form-control" id="searchUserInputValue"
+                                           placeholder="{{'Item.GlobalSearchByValue' | translate }}"
+                                           ng-model="itemInfoSearchValue"
+                                           style="margin-right: 10px;"
+                                           autocomplete="off"/>
+                                    <button type="submit" class="btn btn-primary" ng-click="getItemInfoByKeyAndValue(itemInfoSearchKey, itemInfoSearchValue)">
+                                        {{'Item.GlobalSearch' | translate }}
+                                    </button>
+                                </form>
+                            </div>
+                        </div>
+                        <div style="height: 15px"></div>
+                        <table class="table table-bordered table-striped table-hover" style="width: 100%;table-layout: fixed;word-wrap: break-word;overflow-x: auto;">
+                            <tr>
+                                <th style="text-align: left;width: 10%">{{'Global.App' | translate }}</th>
+                                <th style="text-align: left;width: 10%">{{'Global.Env' | translate }}</th>
+                                <th style="text-align: left;width: 10%">{{'Global.Cluster' | translate }}</th>
+                                <th style="text-align: left;width: 10%">{{'Global.NameSpace' | translate }}</th>
+                                <th style="text-align: left;width: 15%">{{'Global.Key' | translate }}</th>
+                                <th style="text-align: left;width: 35%" id="valueColumn">{{'Global.Value' | translate }}</th>
+                                <th style="text-align: left;width: 10%">{{'Global.Operate' | translate }}</th>
+                            </tr>
+                            <tr ng-repeat="item in pageItemInfo track by $index"  href="#" class="hover cursor-pointer">
+                                <td>{{ item.appId }}</td>
+                                <td>{{ item.envName }}</td>
+                                <td>{{ item.clusterName }}</td>
+                                <td>{{ item.namespaceName }}</td>
+                                <td>
+                                    <span ng-if="isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[$index] == '0'">{{ item.key }}</span>
+                                    <span ng-if="!(isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[$index] == '0')" ng-bind-html="highlightKeyword(item.key,needToBeHighlightedKey)"></span>
+                                </td>
+                                <td>
+                                    <span ng-if="isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[$index] == '0'">{{ item.value }}</span>
+                                    <span ng-if="!(isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[$index] == '0')">
+                                        <span ng-if="isPageItemInfoDisplayValueInARow[$index] == '0'" ng-bind-html="highlightKeyword(item.value,needToBeHighlightedValue)">{{ item.value }}</span>
+                                        <span ng-if="!(isPageItemInfoDisplayValueInARow[$index] == '0')">
+                                            <span ng-show="(isPageItemInfoDisplayValueInARow[$index] == '1') && (!isShowHighlightKeyword[$index])" ng-model="needToBeHighlightedValue">{{ needToBeHighlightedValue }}...</span>
+                                            <span ng-show="(isPageItemInfoDisplayValueInARow[$index] == '2') && (!isShowHighlightKeyword[$index])" ng-model="needToBeHighlightedValue">...{{ needToBeHighlightedValue }}</span>
+                                            <span ng-show="(isPageItemInfoDisplayValueInARow[$index] == '3') && (!isShowHighlightKeyword[$index])" ng-model="needToBeHighlightedValue">...{{ needToBeHighlightedValue }}...</span>
+                                            <a ng-show="!isShowHighlightKeyword[$index]" href="#" ng-click="isShowAllValue($index)">| {{'Global.Expand' | translate }}</a>
+                                            <span ng-show="isShowHighlightKeyword[$index]" ng-bind-html="highlightKeyword(item.value,needToBeHighlightedValue)"></span>
+                                            <a ng-show="isShowHighlightKeyword[$index]" href="#" ng-click="isShowAllValue($index)">| {{'Global.Abbreviate' | translate }}</a>
+                                        </span>
+                                    </span>
+                                </td>
+                                <td>
+                                    <a href="#" ng-click="jumpToTheEditingPage(item.appId,item.envName,item.clusterName)">{{'Global.JumpToEditPage' | translate }}</a>
+                                </td>
+                            </tr>
+                        </table>
+                        <div ng-show="!(pageItemInfo.length === 0)">
+                            <div class="row">
+                                <div class="col-md-10">
+                                    <div class="form-inline" style="display: flex; align-items: center; justify-content: space-between;">
+                                        <div style="margin-right: auto; font-weight: bold;">{{'Paging.TotalItems.part1' | translate }}{{ '&nbsp;' }}{{totalItems}}{{ '&nbsp;' }}{{'Paging.TotalItems.part2' | translate }}</div>
+                                        <select class="form-control" style="margin-right: 10px;" ng-model="pageSize" ng-change="convertPageSizeToInt()">
+                                            <option value="10" selected>10{{ '&nbsp;' }}{{'Paging.DisplayNumber' | translate }}</option>
+                                            <option ng-repeat="size in [20, 50]" value="{{size}}" >{{size}}{{ '&nbsp;' }}{{'Paging.DisplayNumber' | translate }}</option>
+                                        </select>
+                                        <ul class="pagination" style="list-style: none; display: flex; align-items: center;">
+                                            <li style="margin-right: 5px;" ng-class="{disabled: currentPage == 1}">
+                                                <a href="#" ng-click="changePage(1)" style="cursor: pointer;">{{'Paging.PageNumberOne' | translate }}</a>
+                                            </li>
+                                            <li style="margin-right: 5px;" ng-class="{disabled: currentPage == 1}">
+                                                <a href="#" ng-click="changePage(currentPage - 1)" style="cursor: pointer;"><b>«</b></a>
+                                            </li>
+                                            <li ng-repeat="page in pagesArray track by $index" ng-class="{active: page == currentPage, disabled: page === '...'}" style="margin-right: 5px;">
+                                                <a href="#" ng-click="page !== '...' && changePage(page)" style="cursor: pointer;">{{page}}</a>
+                                            </li>
+                                            <li style="margin-right: 5px;" ng-class="{disabled: currentPage == totalPages}">
+                                                <a href="#" ng-click="changePage(currentPage + 1)" style="cursor: pointer;"><b>»</b></a>
+                                            </li>
+                                            <li ng-class="{disabled: currentPage == totalPages}">
+                                                <a href="#" ng-click="changePage(totalPages)" style="cursor: pointer;">{{'Paging.PageNumberLast' | translate }}</a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <div style="text-align: end;"></div>
+                        <div style="height: 15px"></div>
+                        <div style="background-color: white;" ng-show="allItemInfo.length === 0">
+                            <div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
+                                <div><img src="img/nodata.png" /></div>
+                                <div style="margin-left: 5px; color: grey;"><h5>{{'ApolloGlobalSearch.NoData' | translate }}</h5></div>
+                            </div>
+                        </div>
+                    </div>
+                </section>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div ng-include="'views/common/footer.html'"></div>
+
+<!--angular-->
+<script src="vendor/angular/angular.min.js"></script>
+<script src="vendor/angular/angular-resource.min.js"></script>
+<script src="vendor/angular/angular-toastr-1.4.1.tpls.min.js"></script>
+<script src="vendor/angular/loading-bar.min.js"></script>
+<script src="vendor/angular/angular-cookies.min.js"></script>
+
+<script src="vendor/angular/angular-translate.2.18.1/angular-translate.min.js"></script>
+<script src="vendor/angular/angular-translate.2.18.1/angular-translate-loader-static-files.min.js"></script>
+<script src="vendor/angular/angular-translate.2.18.1/angular-translate-storage-cookie.min.js"></script>
+<script src="vendor/angular/angular-sanitize.min.js"></script>
+
+<!-- jquery.js -->
+<script src="vendor/jquery.min.js" type="text/javascript"></script>
+<script src="vendor/select2/select2.min.js" type="text/javascript"></script>
+
+<!-- bootstrap.js -->
+<script src="vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
+<script src="vendor/diff.min.js" type="text/javascript"></script>
+
+<!--valdr-->
+<script src="vendor/valdr/valdr.min.js" type="text/javascript"></script>
+<script src="vendor/valdr/valdr-message.min.js" type="text/javascript"></script>
+
+
+<script type="application/javascript" src="scripts/app.js"></script>
+<script type="application/javascript" src="scripts/services/AppService.js"></script>
+<script type="application/javascript" src="scripts/services/EnvService.js"></script>
+<script type="application/javascript" src="scripts/services/GlobalSearchValueService.js"></script>
+<script type="application/javascript" src="scripts/services/UserService.js"></script>
+<script type="application/javascript" src="scripts/services/CommonService.js"></script>
+<script type="application/javascript" src="scripts/AppUtils.js"></script>
+<script type="application/javascript" src="scripts/services/OrganizationService.js"></script>
+<script type="application/javascript" src="scripts/directive/directive.js"></script>
+<script type="application/javascript" src="scripts/services/PermissionService.js"></script>
+
+<script type="application/javascript" src="scripts/controller/GlobalSearchValueController.js"></script>
+
+<script src="scripts/valdr.js" type="text/javascript"></script>
+
+</body>
+
+</html>

+ 24 - 1
apollo-portal/src/main/resources/static/i18n/en.json

@@ -893,5 +893,28 @@
   "ApolloAuditLog.ParentSpan": "parent operation",
   "ApolloAuditLog.FollowsFromSpan": "last operation",
   "ApolloAuditLog.FieldChangeHistory": "Field Change History",
-  "ApolloAuditLog.InfluenceEntity": "Audit entity influenced"
+  "ApolloAuditLog.InfluenceEntity": "Audit entity influenced",
+  "Global.Title": "Global Search for Value",
+  "Global.App": "App ID",
+  "Global.Env": "Env Name",
+  "Global.Cluster": "Cluster Name",
+  "Global.NameSpace": "NameSpace Name",
+  "Global.Key": "Key",
+  "Global.Value": "Value",
+  "Global.ValueSearch.Tips" : "(Fuzzy search, key can be the name or content of the configuration item, value is the value of the configuration item.)",
+  "Global.Operate" : "Operate",
+  "Global.Expand" : "Expand",
+  "Global.Abbreviate" : "Abbreviate",
+  "Global.JumpToEditPage" : "Jump to edit page",
+  "Item.GlobalSearchByKey": "Search by Key",
+  "Item.GlobalSearchByValue": "Search by Value",
+  "Item.GlobalSearch": "Search",
+  "Item.GlobalSearchSystemError": "System error, please try again or contact the system administrator",
+  "Item.GlobalSearch.Tips": "Search hint",
+  "ApolloGlobalSearch.NoData" : "No data yet, please search or add",
+  "Paging.TotalItems.part1" : "Total of",
+  "Paging.TotalItems.part2" : "records",
+  "Paging.DisplayNumber" : "per/Page",
+  "Paging.PageNumberOne" : "First",
+  "Paging.PageNumberLast" : "Last"
 }

+ 24 - 1
apollo-portal/src/main/resources/static/i18n/zh-CN.json

@@ -893,5 +893,28 @@
   "ApolloAuditLog.ParentSpan": "父操作",
   "ApolloAuditLog.FollowsFromSpan": "前操作",
   "ApolloAuditLog.FieldChangeHistory": "属性变动历史",
-  "ApolloAuditLog.InfluenceEntity": "影响的审计实体"
+  "ApolloAuditLog.InfluenceEntity": "影响的审计实体",
+  "Global.Title": "Value的全局搜索",
+  "Global.App": "应用ID",
+  "Global.Env": "环境",
+  "Global.Cluster": "集群名",
+  "Global.NameSpace": "命名空间",
+  "Global.Key": "Key",
+  "Global.Value": "Value",
+  "Global.ValueSearch.Tips" : "(模糊搜索,key可为配置项名称或content,value为配置项值)",
+  "Global.Operate" : "操作",
+  "Global.Expand" : "展开",
+  "Global.Abbreviate" : "缩略",
+  "Global.JumpToEditPage" : "跳转到编辑页面",
+  "Item.GlobalSearchByKey": "按照Key值检索",
+  "Item.GlobalSearchByValue": "按照Value值检索",
+  "Item.GlobalSearch": "查询",
+  "Item.GlobalSearchSystemError": "系统出错,请重试或联系系统负责人",
+  "Item.GlobalSearch.Tips": "搜索提示",
+  "ApolloGlobalSearch.NoData" : "暂无数据,请进行检索或者添加",
+  "Paging.TotalItems.part1" : "共",
+  "Paging.TotalItems.part2" : "条记录",
+  "Paging.DisplayNumber" : "条/页",
+  "Paging.PageNumberOne" : "首页",
+  "Paging.PageNumberLast" : "尾页"
 }

BIN
apollo-portal/src/main/resources/static/img/nodata.png


+ 2 - 0
apollo-portal/src/main/resources/static/scripts/app.js

@@ -64,6 +64,8 @@ var diff_item_module = angular.module('diff_item', ['app.service', 'apollo.direc
 var namespace_module = angular.module('namespace', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']);
 //server config
 var server_config_manage_module = angular.module('server_config_manage', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
+// Value的全局检索
+var global_search_value_module = angular.module('global_search_value', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'ngSanitize']);
 //setting
 var setting_module = angular.module('setting', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']);
 //role

+ 273 - 0
apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js

@@ -0,0 +1,273 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+global_search_value_module.controller('GlobalSearchValueController',
+    ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'GlobalSearchValueService', 'PermissionService', GlobalSearchValueController]);
+
+function GlobalSearchValueController($scope, $window, $translate, toastr, AppUtil, GlobalSearchValueService, PermissionService) {
+
+    $scope.allItemInfo = [];
+    $scope.pageItemInfo = [];
+    $scope.itemInfoSearchKey = '';
+    $scope.itemInfoSearchValue = '';
+    $scope.needToBeHighlightedKey = '';
+    $scope.needToBeHighlightedValue = '';
+    $scope.isShowHighlightKeyword = [];
+    $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+    $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+    $scope.isAllItemInfoDisplayValueInARow = [];
+    $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+    $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+    $scope.isPageItemInfoDisplayValueInARow = [];
+    $scope.currentPage = 1;
+    $scope.pageSize = '10';
+    $scope.totalItems = 0;
+    $scope.totalPages = 0;
+    $scope.pagesArray = [];
+    $scope.tempKey = '';
+    $scope.tempValue = '';
+
+    $scope.getItemInfoByKeyAndValue = getItemInfoByKeyAndValue;
+    $scope.highlightKeyword = highlightKeyword;
+    $scope.jumpToTheEditingPage = jumpToTheEditingPage;
+    $scope.isShowAllValue = isShowAllValue;
+    $scope.convertPageSizeToInt = convertPageSizeToInt;
+    $scope.changePage = changePage;
+    $scope.getPagesArray = getPagesArray;
+    $scope.determineDisplayKeyOrValueWithoutShowHighlightKeyword = determineDisplayKeyOrValueWithoutShowHighlightKeyword;
+    $scope.determineDisplayValueInARow = determineDisplayValueInARow;
+
+    init();
+    function init() {
+        initPermission();
+    }
+
+    function initPermission() {
+        PermissionService.has_root_permission()
+            .then(function (result) {
+                $scope.isRootUser = result.hasPermission;
+            });
+    }
+
+    function getItemInfoByKeyAndValue(itemInfoSearchKey, itemInfoSearchValue) {
+        $scope.currentPage = 1;
+        $scope.itemInfoSearchKey = itemInfoSearchKey || '';
+        $scope.itemInfoSearchValue = itemInfoSearchValue || '';
+        $scope.allItemInfo = [];
+        $scope.pageItemInfo = [];
+        $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+        $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+        $scope.isAllItemInfoDisplayValueInARow = [];
+        $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+        $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+        $scope.isPageItemInfoDisplayValueInARow = [];
+        $scope.tempKey = itemInfoSearchKey || '';
+        $scope.tempValue = itemInfoSearchValue || '';
+        $scope.isShowHighlightKeyword = [];
+        GlobalSearchValueService.findItemInfoByKeyAndValue($scope.itemInfoSearchKey, $scope.itemInfoSearchValue)
+            .then(handleSuccess).catch(handleError);
+        function handleSuccess(result) {
+            let allItemInfo = [];
+            let isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+            let isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+            let isAllItemInfoDisplayValueInARow = [];
+            if(($scope.itemInfoSearchKey === '') && !($scope.itemInfoSearchValue === '')){
+                $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue;
+                $scope.needToBeHighlightedKey = '';
+                result.body.forEach((itemInfo, index) => {
+                    allItemInfo.push(itemInfo);
+                    isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = '0';
+                    isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue);
+                    isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue);
+                });
+            }else if(!($scope.itemInfoSearchKey === '') && ($scope.itemInfoSearchValue === '')){
+                $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey;
+                $scope.needToBeHighlightedValue = '';
+                result.body.forEach((itemInfo, index) => {
+                    allItemInfo.push(itemInfo);
+                    isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey);
+                    isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = '0';
+                });
+            }else{
+                $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey;
+                $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue;
+                result.body.forEach((itemInfo, index) => {
+                    allItemInfo.push(itemInfo);
+                    isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue);
+                    isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey);
+                    isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue);
+                });
+            }
+            $scope.totalItems = allItemInfo.length;
+            $scope.allItemInfo = allItemInfo;
+            $scope.totalPages = Math.ceil($scope.totalItems / parseInt($scope.pageSize, 10));
+            const startIndex = ($scope.currentPage - 1) * parseInt($scope.pageSize, 10);
+            const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), allItemInfo.length);
+            $scope.pageItemInfo = allItemInfo.slice(startIndex, endIndex);
+            $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword;
+            $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword;
+            $scope.isAllItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow;
+            $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+            $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+            $scope.isPageItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex);
+            getPagesArray();
+            if(result.hasMoreData){
+                toastr.warning(result.message, $translate.instant('Item.GlobalSearch.Tips'));
+            }
+        }
+
+        function handleError(error) {
+            $scope.itemInfo = [];
+            toastr.error(AppUtil.errorMsg(error), $translate.instant('Item.GlobalSearchSystemError'));
+        }
+    }
+
+    function convertPageSizeToInt() {
+        getItemInfoByKeyAndValue($scope.tempKey, $scope.tempValue);
+    }
+
+    function changePage(page) {
+        if (page >= 1 && page <= $scope.totalPages) {
+            $scope.currentPage = page;
+            $scope.isShowHighlightKeyword = [];
+            $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+            $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+            $scope.isPageItemInfoDisplayValueInARow = [];
+            $scope.itemInfoSearchKey = $scope.tempKey;
+            $scope.itemInfoSearchValue = $scope.tempValue;
+            const startIndex = ($scope.currentPage - 1)* parseInt($scope.pageSize, 10);
+            const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), $scope.totalItems);
+            $scope.pageItemInfo = $scope.allItemInfo.slice(startIndex, endIndex);
+            $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+            $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+            $scope.isPageItemInfoDisplayValueInARow = $scope.isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex);
+            getPagesArray();
+        }
+    }
+
+    function getPagesArray() {
+        const pageRange = 2;
+        let pagesArray = [];
+        let currentPage = $scope.currentPage;
+        let totalPages = $scope.totalPages;
+        if (totalPages <= (pageRange * 2) + 4) {
+            for (let i = 1; i <= totalPages; i++) {
+                pagesArray.push(i);
+            }
+        } else {
+            if (currentPage <= (pageRange + 2)) {
+                for (let i = 1; i <= pageRange * 2 + 2; i++) {
+                    pagesArray.push(i);
+                }
+                pagesArray.push('...');
+                pagesArray.push(totalPages);
+            } else if (currentPage >= (totalPages - (pageRange + 1))) {
+                for (let i = totalPages - pageRange * 2 - 1 ; i <= totalPages; i++) {
+                    pagesArray.push(i);
+                }
+                pagesArray.unshift('...');
+                pagesArray.unshift(1);
+            } else {
+                for (let i = (currentPage - pageRange); i <= currentPage + pageRange; i++) {
+                    pagesArray.push(i);
+                }
+                pagesArray.unshift('...');
+                pagesArray.unshift(1);
+                pagesArray.push('...');
+                pagesArray.push(totalPages);
+            }
+        }
+        $scope.pagesArray = pagesArray;
+    }
+
+    function determineDisplayValueInARow(value, highlight) {
+        var valueColumn = document.getElementById('valueColumn');
+        var testElement = document.createElement('span');
+        setupTestElement(testElement, valueColumn);
+        testElement.innerText = value;
+        document.body.appendChild(testElement);
+        const position = determinePosition(value, highlight);
+        let displayValue = '0';
+        if (testElement.scrollWidth > testElement.offsetWidth) {
+            displayValue = position;
+        } else {
+            if (testElement.scrollWidth === testElement.offsetWidth) {
+                return '0';
+            }
+            switch (position) {
+                case '1':
+                    testElement.innerText = value + '...' + '| ' + $translate.instant('Global.Expand');
+                    break;
+                case '2':
+                    testElement.innerText = '...' + value + '| ' + $translate.instant('Global.Expand');
+                    break;
+                case '3':
+                    testElement.innerText = '...' + value + '...' + '| ' + $translate.instant('Global.Expand');
+                    break;
+                default:
+                    return '0';
+            }
+            if (testElement.scrollWidth === testElement.offsetWidth) {
+                displayValue = '0';
+            } else {
+                displayValue = position;
+            }
+        }
+        document.body.removeChild(testElement);
+        return displayValue;
+    }
+
+    function setupTestElement(element, valueColumn) {
+        element.style.visibility = 'hidden';
+        element.style.position = 'absolute';
+        element.style.whiteSpace = 'nowrap';
+        element.style.display = 'inline-block';
+        element.style.fontFamily = '"Open Sans", sans-serif';
+        const devicePixelRatio = window.devicePixelRatio;
+        const zoomLevel = Math.round((window.outerWidth / window.innerWidth) * 100) / 100;
+        element.style.fontSize = 13 * devicePixelRatio * zoomLevel + 'px';
+        element.style.padding = 8 * devicePixelRatio * zoomLevel + 'px';
+        element.style.width = valueColumn.offsetWidth * devicePixelRatio * zoomLevel + 'px';
+    }
+
+    function determinePosition(value, highlight) {
+        const position = value.indexOf(highlight);
+        if (position === -1) return '-1';
+        if (position === 0) return '1';
+        if (position + highlight.length === value.length) return '2';
+        return "3";
+    }
+
+    function determineDisplayKeyOrValueWithoutShowHighlightKeyword(keyorvalue, highlight) {
+        return keyorvalue === highlight ? '0' : '-1';
+    }
+
+    function jumpToTheEditingPage(appid,env,cluster){
+        let url = AppUtil.prefixPath() + "/config.html#/appid=" + appid + "&" +"env=" + env + "&" + "cluster=" + cluster;
+        window.open(url, '_blank');
+    }
+
+    function highlightKeyword(fulltext,keyword) {
+        if (!keyword || keyword.length === 0) return fulltext;
+        let regex = new RegExp("(" + keyword + ")", "g");
+        return fulltext.replace(regex, '<span class="highlight" style="background: yellow;padding: 1px 4px;">$1</span>');
+    }
+
+    function isShowAllValue(index){
+        $scope.isShowHighlightKeyword[index] = !$scope.isShowHighlightKeyword[index];
+    }
+
+}

+ 40 - 0
apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+appService.service('GlobalSearchValueService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) {
+    let global_search_resource = $resource('', {}, {
+        get_item_Info_by_key_and_Value: {
+            isArray: false,
+            method: 'GET',
+            url: AppUtil.prefixPath() + '/global-search/item-info/by-key-or-value',
+            params: {
+                key: 'key',
+                value: 'value'
+            }
+        }
+    });
+    return {
+        findItemInfoByKeyAndValue:function (key,value){
+            let d = $q.defer();
+            global_search_resource.get_item_Info_by_key_and_Value({key: key,value: value},function (result) {
+                d.resolve(result);
+            }, function (error) {
+                d.reject(error);
+            });
+            return d.promise;
+        }
+    }
+}]);

+ 1 - 0
apollo-portal/src/main/resources/static/views/common/nav.html

@@ -66,6 +66,7 @@
                         <li><a href="{{ '/system_info.html' | prefixPath }}" target="_blank">{{'Common.Nav.SystemInfo' | translate }}</a></li>
                         <li><a href="{{ '/config_export.html' | prefixPath }}" target="_blank">{{'Common.Nav.ConfigExport' | translate }}</a></li>
                         <li><a href="{{ '/audit_log_menu.html' | prefixPath }}" target="_blank">{{'ApolloAuditLog.Title' | translate }}</a></li>
+                        <li><a href="{{ '/global_search_value.html' | prefixPath }}" target="_blank">{{'Global.Title' | translate }}</a></li>
                     </ul>
                 </li>
 

+ 135 - 0
apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.portal.controller;
+
+/**
+ * @author hujiyuan 2024-08-10
+ */
+
+import com.ctrip.framework.apollo.common.http.SearchResponseEntity;
+import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
+import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo;
+import com.ctrip.framework.apollo.portal.service.GlobalSearchService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.*;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GlobalSearchControllerTest {
+
+    private MockMvc mockMvc;
+
+    @Mock
+    private PortalConfig portalConfig;
+
+    @Mock
+    private GlobalSearchService globalSearchService;
+
+    @InjectMocks
+    private GlobalSearchController globalSearchController;
+
+    private final int perEnvSearchMaxResults = 200;
+
+    @Before
+    public void setUp() {
+        when(portalConfig.getPerEnvSearchMaxResults()).thenReturn(perEnvSearchMaxResults);
+        mockMvc = MockMvcBuilders.standaloneSetup(globalSearchController).build();
+    }
+
+    @Test
+    public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnEmptyItemInfos() throws Exception {
+        when(globalSearchService.getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(new ArrayList<>()));
+        mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .param("key", "query-key")
+                        .param("value", "query-value"))
+                .andExpect(status().isOk())
+                .andExpect(content().json("{\"body\":[],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}"));
+        verify(portalConfig,times(1)).getPerEnvSearchMaxResults();
+        verify(globalSearchService,times(1)).getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults));
+    }
+
+    @Test
+    public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos_ButOverPerEnvLimit() throws Exception {
+        List<ItemInfo> allEnvMockItemInfos = new ArrayList<>();
+        allEnvMockItemInfos.add(new ItemInfo("appid1","env1","cluster1","namespace1","query-key","query-value"));
+        allEnvMockItemInfos.add(new ItemInfo("appid2","env2","cluster2","namespace2","query-key","query-value"));
+        when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.okWithMessage(allEnvMockItemInfos,"In DEV , PRO , more than "+perEnvSearchMaxResults+" items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope."));
+        mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .param("key", "query-key")
+                        .param("value", "query-value"))
+                .andExpect(status().isOk())
+                .andExpect(content().json("{\"body\":[" +
+                        "            { \"appId\": \"appid1\",\n" +
+                        "            \"envName\": \"env1\",\n" +
+                        "            \"clusterName\": \"cluster1\",\n" +
+                        "            \"namespaceName\": \"namespace1\",\n" +
+                        "            \"key\": \"query-key\",\n" +
+                        "            \"value\": \"query-value\"}," +
+                        "            { \"appId\": \"appid2\",\n" +
+                        "            \"envName\": \"env2\",\n" +
+                        "            \"clusterName\": \"cluster2\",\n" +
+                        "            \"namespaceName\": \"namespace2\",\n" +
+                        "            \"key\": \"query-key\",\n" +
+                        "            \"value\": \"query-value\"}],\"hasMoreData\":true,\"message\":\"In DEV , PRO , more than 200 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.\",\"code\":200}"));
+        verify(portalConfig,times(1)).getPerEnvSearchMaxResults();
+        verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults));
+    }
+
+    @Test
+    public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos() throws Exception {
+        List<ItemInfo> allEnvMockItemInfos = new ArrayList<>();
+        allEnvMockItemInfos.add(new ItemInfo("appid1","env1","cluster1","namespace1","query-key","query-value"));
+        allEnvMockItemInfos.add(new ItemInfo("appid2","env2","cluster2","namespace2","query-key","query-value"));
+        when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(allEnvMockItemInfos));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .param("key", "query-key")
+                        .param("value", "query-value"))
+                .andExpect(status().isOk())
+                .andExpect(content().json("{\"body\":[" +
+                        "            { \"appId\": \"appid1\",\n" +
+                        "            \"envName\": \"env1\",\n" +
+                        "            \"clusterName\": \"cluster1\",\n" +
+                        "            \"namespaceName\": \"namespace1\",\n" +
+                        "            \"key\": \"query-key\",\n" +
+                        "            \"value\": \"query-value\"}," +
+                        "            { \"appId\": \"appid2\",\n" +
+                        "            \"envName\": \"env2\",\n" +
+                        "            \"clusterName\": \"cluster2\",\n" +
+                        "            \"namespaceName\": \"namespace2\",\n" +
+                        "            \"key\": \"query-key\",\n" +
+                        "            \"value\": \"query-value\"}],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}"));
+        verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults));
+    }
+
+}

+ 127 - 0
apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed 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.
+ *
+ */
+package com.ctrip.framework.apollo.portal.service;
+
+/**
+ * @author hujiyuan 2024-08-10
+ */
+
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
+import com.ctrip.framework.apollo.common.dto.PageDTO;
+import com.ctrip.framework.apollo.common.http.SearchResponseEntity;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
+import com.ctrip.framework.apollo.portal.component.PortalSettings;
+import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo;
+import com.ctrip.framework.apollo.portal.environment.Env;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.domain.PageRequest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GlobalSearchServiceTest {
+
+    @Mock
+    private AdminServiceAPI.ItemAPI itemAPI;
+
+    @Mock
+    private PortalSettings portalSettings;
+
+    @InjectMocks
+    private GlobalSearchService globalSearchService;
+
+    private final List<Env> activeEnvs = new ArrayList<>();
+
+    @Before
+    public void setUp() {
+        when(portalSettings.getActiveEnvs()).thenReturn(activeEnvs);
+    }
+
+    @Test
+    public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos() {
+        activeEnvs.add(Env.DEV);
+        activeEnvs.add(Env.PRO);
+
+        ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp","TestCluster","TestNamespace","TestKey","TestValue");
+        List<ItemInfoDTO> mockItemInfoDTOs = new ArrayList<>();
+        mockItemInfoDTOs.add(itemInfoDTO);
+        Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 1L));
+        SearchResponseEntity<List<ItemInfo>> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1);
+        assertEquals(2, mockItemInfos.getBody().size());
+
+        List<ItemInfo> devMockItemInfos = new ArrayList<>();
+        List<ItemInfo> proMockItemInfos = new ArrayList<>();
+        List<ItemInfo> allEnvMockItemInfos = new ArrayList<>();
+        devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+        proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+        allEnvMockItemInfos.addAll(devMockItemInfos);
+        allEnvMockItemInfos.addAll(proMockItemInfos);
+
+        verify(itemAPI,times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1));
+        verify(portalSettings,times(1)).getActiveEnvs();
+        assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString());
+    }
+
+    @Test
+    public void testGet_PerEnv_ItemInfo_withKeyAndValue_BySearch_ReturnEmptyItemInfos() {
+        activeEnvs.add(Env.DEV);
+        activeEnvs.add(Env.PRO);
+        Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), anyString(), anyString(), eq(0), eq(1)))
+                .thenReturn(new PageDTO<>(new ArrayList<>(), PageRequest.of(0, 1), 0L));
+        SearchResponseEntity<List<ItemInfo>> result = globalSearchService.getAllEnvItemInfoBySearch("NonExistentKey", "NonExistentValue", 0, 1);
+        assertEquals(0, result.getBody().size());
+    }
+
+    @Test
+    public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos_ButOverPerEnvLimit() {
+        activeEnvs.add(Env.DEV);
+        activeEnvs.add(Env.PRO);
+
+        ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp","TestCluster","TestNamespace","TestKey","TestValue");
+        List<ItemInfoDTO> mockItemInfoDTOs = new ArrayList<>();
+        mockItemInfoDTOs.add(itemInfoDTO);
+        Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 2L));
+        SearchResponseEntity<List<ItemInfo>> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1);
+        assertEquals(2, mockItemInfos.getBody().size());
+
+        List<ItemInfo> devMockItemInfos = new ArrayList<>();
+        List<ItemInfo> proMockItemInfos = new ArrayList<>();
+        List<ItemInfo> allEnvMockItemInfos = new ArrayList<>();
+        devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+        proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+        allEnvMockItemInfos.addAll(devMockItemInfos);
+        allEnvMockItemInfos.addAll(proMockItemInfos);
+        String message = "In DEV , PRO , more than 1 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.";
+        verify(itemAPI,times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1));
+        verify(portalSettings,times(1)).getActiveEnvs();
+        assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString());
+        assertEquals(message, mockItemInfos.getMessage());
+    }
+
+}

+ 13 - 0
changes/changes-2.4.0.md

@@ -0,0 +1,13 @@
+Changes by Version
+==================
+Release Notes.
+
+Apollo 2.4.0
+
+------------------
+* [Update the server config link in system info page](https://github.com/apolloconfig/apollo/pull/5204)
+* [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200)
+* [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182)
+
+------------------
+All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)

+ 4 - 0
docs/en/README.md

@@ -37,6 +37,10 @@ Demo Environment:
 * **Grayscale release**
     * Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem
 
+- **Global Search Configuration Items**
+  - A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used
+  - It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations
+
 * **Authorization management, release approval and operation audit**
     * Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors
     * All operations have audit logs for easy tracking of problems

+ 11 - 3
docs/en/deployment/distributed-deployment-guide.md

@@ -769,9 +769,9 @@ apollo.service.registry.cluster=same name with apollo Cluster
 ```
 
 2. (optional) If you want to customize Config Service and Admin Service's uri for Client, 
-for example when deploying on the intranet, 
-if you don't want to expose the intranet ip, 
-you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package
+   for example when deploying on the intranet, 
+   if you don't want to expose the intranet ip, 
+   you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package
     ```properties
     apollo.service.registry.uri=http://your-ip-or-domain:${server.port}/
     ```
@@ -1447,6 +1447,14 @@ The default is true, which makes it easy to quickly search for configurations by
 
 If set to false, this feature is disabled
 
+### 3.1.14 apollo.portal.search.perEnvMaxResults - set the Administrator Tool-Global Search for Value function's maximum number of search results for a single individual environment 
+
+> For versions 2.4.0 and above
+
+Default is 200, which means that each environment will return up to 200 results in a single search operation.
+
+Modifying this parameter may affect the performance of the search function, so before modifying it, you should conduct sufficient testing and adjust the value of `apollo.portal.search.perEnvMaxResults` appropriately according to the actual business requirements and system resources to balance the performance and the number of search results.
+
 ## 3.2 Adjusting ApolloConfigDB configuration
 
 Configuration items are uniformly stored in the ApolloConfigDB.ServerConfig table. It should be noted that each environment's ApolloConfigDB.ServerConfig needs to be configured separately, and the modification takes effect in real time for one minute afterwards.

+ 1 - 1
docs/en/design/apollo-design.md

@@ -130,7 +130,7 @@ Why do we use Eureka as a service registry instead of the traditional zk and etc
 ### 1.3.2 Admin Service
 
 * Provide configuration management interface
-* Provides interfaces for configuration modification, publishing, etc.
+* Provides interfaces for configuration modification, publishing, retrieval, etc.
 * Interface service object is Portal
 
 ### 1.3.3 Meta Server

+ 6 - 0
docs/en/design/apollo-introduction.md

@@ -78,7 +78,13 @@ It is precisely based on the particularity of configuration that Apollo has been
 * **Client configuration information monitoring**
   * You can easily see which instances the configuration is being used on the interface
 
+**Global Search Configuration Items**
+
+- A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used
+- It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations
+
 **Java and .Net native clients available**
+
   * Provides native clients of Java and .Net for easy application integration
   * Support Spring Placeholder, Annotation and Spring Boot's ConfigurationProperties for easy application use (requires Spring 3.1.1+)
   * Also provides Http interface, non-Java and .Net applications can also be easily used

BIN
docs/en/images/Configuration query-Non properties.png


BIN
docs/en/images/Configuration query-properties.png


BIN
docs/en/images/System-parameterization-of-global-search-configuration-items.png


+ 27 - 0
docs/en/portal/apollo-user-guide.md

@@ -133,6 +133,20 @@ The rollback mechanism here is similar to the release system, where the rollback
 
 The rollback in Apollo is a similar mechanism. Clicking rollback rolls back the configuration published to the client to the previous published version, which means that the configuration read by the client will be restored to the previous version, but the configuration in the edited state on the page will not be rolled back, so that the developer can re-publish after fixing the configuration.
 
+## 1.7 Configuration queries (administrator privileges)
+
+After a configuration has been added or modified, the administrator user can make a query for the configuration item it belongs to as well as jump to modifications by going to the `Administrator Tools - Global Search for Value` page.
+
+The query here is a fuzzy search, where at least one of the key and value of the configuration item is searched to find out in which application, environment, cluster, namespace the configuration is used.
+
+- Properties format configuration can be retrieved directly from the key and value
+
+![Configuration query-properties](../images/Configuration query-properties.png)
+
+- xml, json, yml, yaml, txt and other formats configuration, because the storage of content-value storage, so you can key = content, value = configuration item content, retrieval
+
+![Configuration query-Non properties](../images/Configuration query-Non properties.png)
+
 # II. Public component access guide
 
 ## 2.1 Difference between public components and common applications
@@ -482,6 +496,19 @@ Apollo has added an access key mechanism since version 1.6.0, so that only authe
 
 3. Client-side [configure access key](en/client/java-sdk-user-guide?id=_1244-configuring-access-keys) .
 
+## 6.3 System parameterization of global search configuration items
+
+Starting from version 2.4.0, apollo-portal adds the ability to globally search for configuration items by fuzzy retrieval of the key and value of a configuration item to find out which application, environment, cluster, or namespace the configuration item with the corresponding value is used in. In order to prevent memory overflow (OOM) problems when performing global view searches of configuration items, we introduce a system parameter `apollo.portal.search.perEnvMaxResults`, which is used to limit the number of maximum search results per environment configuration item in a single search. By default, this value is set to `200`, but administrators can adjust it to suit their actual needs.
+
+**Setting method:**
+
+1. Log in to the Apollo Configuration Center interface with a super administrator account.
+2. Just go to the `Administrator Tools - System Parameters` page and add or modify the `apollo.portal.search.perEnvMaxResults` configuration item.
+
+Please note that modifications to system parameters may affect the performance of the search function, so you should perform adequate testing and ensure that you understand exactly what the parameters do before making changes.
+
+![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png)
+
 # VII. Best practices
 
 ## 7.1 Security Related

+ 4 - 0
docs/zh/README.md

@@ -40,6 +40,10 @@ Java客户端不依赖任何框架,能够运行于所有Java运行时环境,
 * **灰度发布**
   * 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。
 
+- **配置项的全局视角搜索**
+  - 通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用。
+  - 通过高亮显示、分页与跳转配置等操作,便于让管理员以及SRE角色快速、便捷地找到与更改资源的配置值。
+
 * **权限管理、发布审核、操作审计**
   * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
   * 所有的操作都有审计日志,可以方便的追踪问题。

+ 8 - 0
docs/zh/deployment/distributed-deployment-guide.md

@@ -1392,6 +1392,14 @@ portal上“帮助”链接的地址,默认是Apollo github的wiki首页,可
 
 如果设置为 false,则关闭此功能
 
+### 3.1.14 apollo.portal.search.perEnvMaxResults - 设置管理员工具-value的全局搜索功能单次单独环境最大搜索结果的数量
+
+> 适用于2.4.0及以上版本
+
+默认为200,意味着每个环境在单次搜索操作中最多返回200条结果
+
+修改该参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,根据实际业务需求和系统资源情况,适当调整`apollo.portal.search.perEnvMaxResults`的值,以平衡性能和搜索结果的数量
+
 
 ## 3.2 调整ApolloConfigDB配置
 配置项统一存储在ApolloConfigDB.ServerConfig表中,需要注意每个环境的ApolloConfigDB.ServerConfig都需要单独配置,修改完一分钟实时生效。

+ 1 - 1
docs/zh/design/apollo-design.md

@@ -136,7 +136,7 @@ sequenceDiagram
 ### 1.3.2 Admin Service
 
 * 提供配置管理接口
-* 提供配置修改、发布等接口
+* 提供配置修改、发布、检索等接口
 * 接口服务对象为Portal
 
 ### 1.3.3 Meta Server

+ 15 - 11
docs/zh/design/apollo-introduction.md

@@ -69,26 +69,30 @@ Apollo支持4个维度管理Key-Value格式的配置:
 * **灰度发布**
 	* 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例
 
+* **配置项的全局视角搜索**
+	* 通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用
+	* 通过高亮显示、分页与跳转配置等操作,便于让管理员以及SRE角色快速、便捷地找到与更改资源的配置值
+
 * **权限管理、发布审核、操作审计**
-	* 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
-	* 所有的操作都有审计日志,可以方便地追踪问题
+  * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
+  * 所有的操作都有审计日志,可以方便地追踪问题
 
 * **客户端配置信息监控**
-	* 可以在界面上方便地看到配置在被哪些实例使用
+  * 可以在界面上方便地看到配置在被哪些实例使用
 
 * **提供Java和.Net原生客户端**
-	* 提供了Java和.Net的原生客户端,方便应用集成
-	* 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)
-	* 同时提供了Http接口,非Java和.Net应用也可以方便地使用
+  * 提供了Java和.Net的原生客户端,方便应用集成
+  * 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)
+  * 同时提供了Http接口,非Java和.Net应用也可以方便地使用
 
 * **提供开放平台API**
-	* Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等
-	* 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
+  * Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等
+  * 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
 
 * **部署简单**
-	* 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
-	* 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来
-	* Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数
+  * 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
+  * 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来
+  * Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数
 
 # 3、Apollo at a glance
 

BIN
docs/zh/images/Configuration query-Non properties.png


BIN
docs/zh/images/Configuration query-properties.png


BIN
docs/zh/images/System-parameterization-of-global-search-configuration-items.png


+ 31 - 4
docs/zh/portal/apollo-user-guide.md

@@ -123,6 +123,20 @@ Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文
 
 Apollo中的回滚也是类似的机制,点击回滚后是将发布到客户端的配置回滚到上一个已发布版本,也就是说客户端读取到的配置会恢复到上一个版本,但页面上编辑状态的配置是不会回滚的,从而开发可以在修复配置后重新发布。
 
+## 1.7 配置查询(管理员权限)
+
+在配置添加或修改后,管理员用户可以通过进入 `管理员工具 - Value的全局搜索` 页面,来对配置项进行所属查询以及跳转修改。
+
+这里的查询为模糊检索,通过对配置项的key与value至少一项进行检索,找到该配置在哪个应用、环境、集群、命名空间中被使用。
+
+- properties格式配置可以直接通过对key与value进行检索
+
+![Configuration query-properties](../images/Configuration query-properties.png)
+
+- xml、json、yml、yaml、txt等格式配置,由于存储时以content-value进行存储,故可以通过key=content、value=配置项内容,进行检索
+
+![Configuration query-Non properties](../images/Configuration query-Non properties.png)
+
 # 二、公共组件接入指南
 ## 2.1 公共组件和普通应用的区别
 
@@ -226,9 +240,9 @@ Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文
 
 3. 关联成功后,页面会自动跳转到Namespace权限管理页面
     1. 分配修改权限
-![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png)
+    ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png)
     2. 分配发布权限
-![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png)
+    ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png)
 
 4. 点击“返回”回到项目页面
 
@@ -448,13 +462,26 @@ Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文
 Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验证的客户端才能访问敏感配置。如果应用开启了访问密钥,客户端需要配置密钥,否则无法获取配置。
 
 1. 项目管理员打开管理密钥页面
-![管理密钥入口](https://user-images.githubusercontent.com/837658/94990081-f4d3cd80-05ab-11eb-9470-fed5ec6de92e.png)
+    ![管理密钥入口](https://user-images.githubusercontent.com/837658/94990081-f4d3cd80-05ab-11eb-9470-fed5ec6de92e.png)
 
 2. 为项目的每个环境生成访问密钥,注意默认是禁用的,建议在客户端都配置完成后再开启
-![密钥配置页面](https://user-images.githubusercontent.com/837658/94990150-788dba00-05ac-11eb-9a12-727fdb872e42.png)
+    ![密钥配置页面](https://user-images.githubusercontent.com/837658/94990150-788dba00-05ac-11eb-9a12-727fdb872e42.png)
 
 3. 客户端侧[配置访问密钥](zh/client/java-sdk-user-guide#_1244-配置访问密钥)
 
+## 6.3 全局搜索配置项的系统参数设置
+
+从2.4.0版本开始,apollo-portal增加了全局搜索配置项的功能,通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用。为了防止在进行配置项的全局视角搜索时出现内存溢出(OOM)的问题,我们引入了一个系统参数`apollo.portal.search.perEnvMaxResults`。这个参数用于限制每个环境配置项单次最大搜索结果的数量。默认情况下,这个值被设置为`200`,但管理员可以根据实际需求进行调整。
+
+**设置方法:**
+
+1. 用超级管理员账号登录到Apollo配置中心的界面
+2. 进入`管理员工具 - 系统参数`页面新增或修改`apollo.portal.search.perEnvMaxResults`配置项即可
+
+请注意,修改系统参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,并确保理解参数的具体作用。
+
+![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png)
+
 # 七、最佳实践
 
 ## 7.1 安全相关