This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 4ec7d37ac feat(instance): CLOUD type rename and one-click cloud
instance import
4ec7d37ac is described below
commit 4ec7d37ac5e3b368bcd1f194f8eb73d05ff9e6b8
Author: lizhimins <[email protected]>
AuthorDate: Thu Aug 20 10:38:28 2026 +0800
feat(instance): CLOUD type rename and one-click cloud instance import
Rename the legacy PROXY instance type to CLOUD (displayed as 云服务),
drop the all-proxy filter grouping, and reject CLOUD for manually
created APACHE instances. Add one-click import: POST
/api/instances/import-cloud walks every catalog region of a credential
and imports all instances idempotently, naming them by cloud instance
id and auto-filling remarks from the cloud detail; the add-instance
modal shows the button in the footer left of 取消.
---
.../studio/common/domain/enums/InstanceType.java | 12 +-
.../CloudImportResultVO.java} | 38 +--
.../ImportCloudInstancesDTO.java} | 24 +-
.../studio/instance/InstanceController.java | 5 +
.../rocketmq/studio/instance/InstanceService.java | 92 +++++++-
.../instance/MybatisPlusInstanceRepository.java | 15 +-
.../provider/apache/RocketMQDashboardProvider.java | 2 +-
server/src/main/resources/db/schema.sql | 2 +-
.../instance/InstanceCapabilityServiceTest.java | 4 +-
.../studio/instance/InstanceControllerTest.java | 18 +-
.../studio/instance/InstanceServiceTest.java | 214 ++++++++++++++---
.../MybatisPlusInstanceRepositoryTest.java | 31 ++-
web/src/api/instance.test.ts | 8 +-
web/src/api/instance.ts | 14 +-
web/src/hooks/useInstanceFilter.test.tsx | 2 +-
web/src/i18n/translations.ts | 1 -
web/src/layouts/MainLayout.test.tsx | 2 +-
.../pages/instance/__tests__/ConsumerPage.test.tsx | 6 +-
.../pages/instance/__tests__/InstancePage.test.tsx | 57 ++++-
.../pages/instance/__tests__/TopicPage.test.tsx | 16 +-
web/src/pages/instance/index.tsx | 260 +++++++++++++--------
web/src/pages/instance/topic.tsx | 6 +-
web/src/services/instanceService.test.ts | 11 +-
web/src/services/instanceService.ts | 19 +-
24 files changed, 606 insertions(+), 253 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
b/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
index 4afd4c880..a0716f70d 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
@@ -18,17 +18,9 @@
package org.apache.rocketmq.studio.common.domain.enums;
public enum InstanceType {
- /** Legacy generic Proxy value retained for persisted and cloud-managed
instances. */
- PROXY,
+ /** Cloud-managed (vendor-hosted) instance selected from the vendor
catalog. */
+ CLOUD,
PROXY_LOCAL,
PROXY_CLUSTER,
DIRECT;
-
- public boolean isProxy() {
- return this != DIRECT;
- }
-
- public InstanceType normalizeApacheType() {
- return this == PROXY ? PROXY_CLUSTER : this;
- }
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/CloudImportResultVO.java
similarity index 66%
copy from
server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
copy to
server/src/main/java/org/apache/rocketmq/studio/instance/CloudImportResultVO.java
index 4afd4c880..5f453923f 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/CloudImportResultVO.java
@@ -15,20 +15,26 @@
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.domain.enums;
-
-public enum InstanceType {
- /** Legacy generic Proxy value retained for persisted and cloud-managed
instances. */
- PROXY,
- PROXY_LOCAL,
- PROXY_CLUSTER,
- DIRECT;
-
- public boolean isProxy() {
- return this != DIRECT;
- }
-
- public InstanceType normalizeApacheType() {
- return this == PROXY ? PROXY_CLUSTER : this;
- }
+package org.apache.rocketmq.studio.instance;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CloudImportResultVO {
+
+ private int discovered;
+
+ private int imported;
+
+ private int skipped;
+
+ private List<String> failed;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/ImportCloudInstancesDTO.java
similarity index 66%
copy from
server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
copy to
server/src/main/java/org/apache/rocketmq/studio/instance/ImportCloudInstancesDTO.java
index 4afd4c880..90c7eab08 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceType.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/ImportCloudInstancesDTO.java
@@ -15,20 +15,18 @@
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.domain.enums;
+package org.apache.rocketmq.studio.instance;
-public enum InstanceType {
- /** Legacy generic Proxy value retained for persisted and cloud-managed
instances. */
- PROXY,
- PROXY_LOCAL,
- PROXY_CLUSTER,
- DIRECT;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
- public boolean isProxy() {
- return this != DIRECT;
- }
+@Data
+public class ImportCloudInstancesDTO {
- public InstanceType normalizeApacheType() {
- return this == PROXY ? PROXY_CLUSTER : this;
- }
+ @NotNull
+ private InstanceVendor vendor;
+
+ @NotNull
+ private Long credentialId;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
index d3dbb0ed2..2d271b5a1 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
@@ -58,6 +58,11 @@ public class InstanceController {
return
Result.ok(instanceService.createInstance(request.toInstanceVO()));
}
+ @PostMapping("/import-cloud")
+ public Result<CloudImportResultVO> importCloudInstances(@Valid
@RequestBody ImportCloudInstancesDTO request) {
+ return
Result.ok(instanceService.importCloudInstances(request.getVendor(),
request.getCredentialId()));
+ }
+
@PostMapping("/update")
public Result<InstanceVO> updateInstance(@Valid @RequestBody
UpdateInstanceDTO request) {
InstanceVO vo = request.toInstanceVO();
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
index 00c0db05a..40f8c8cfb 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
@@ -26,7 +26,10 @@ import
org.apache.rocketmq.studio.audit.OperationAuditService;
import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.provider.CloudCatalogProvider;
import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
+import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
+import org.apache.rocketmq.studio.provider.CloudRegionVO;
import org.apache.rocketmq.studio.provider.InstanceProvider;
import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
import org.apache.rocketmq.studio.settings.DataSourceVO;
@@ -116,6 +119,78 @@ public class InstanceService {
return saved;
}
+ /**
+ * Imports every cloud instance visible to the credential by walking all
catalog regions.
+ * Remarks are resolved from the cloud instance detail during creation.
Instances whose
+ * resolved name already exists are skipped; per-instance failures are
collected instead
+ * of aborting the batch.
+ */
+ public CloudImportResultVO importCloudInstances(InstanceVendor vendor,
Long credentialId) {
+ if (vendor == null || vendor == InstanceVendor.APACHE) {
+ throw new BusinessException(400, "Import is only supported for
cloud vendors");
+ }
+ if (credentialId == null) {
+ throw new BusinessException(400, "credentialId is required");
+ }
+ CloudCredentialVO credential =
cloudCredentialRepository.findById(credentialId)
+ .orElseThrow(() -> new BusinessException(404, "Cloud
credential not found: " + credentialId));
+ if (credential.getVendor() != vendor) {
+ throw new BusinessException(400, "Cloud credential vendor does not
match " + vendor);
+ }
+ CloudCatalogProvider catalog = providerRegistry.catalogFor(vendor);
+
+ int discovered = 0;
+ int imported = 0;
+ int skipped = 0;
+ List<String> failed = new ArrayList<>();
+ for (CloudRegionVO region : catalog.listRegions(credentialId)) {
+ if (region == null || !StringUtils.hasText(region.getRegionId())) {
+ continue;
+ }
+ List<CloudInstanceOptionVO> options;
+ try {
+ options = catalog.listCloudInstances(credentialId,
region.getRegionId(), null);
+ } catch (BusinessException ex) {
+ failed.add(region.getRegionId() + ": " + ex.getMessage());
+ continue;
+ }
+ for (CloudInstanceOptionVO option : options) {
+ if (option == null ||
!StringUtils.hasText(option.getInstanceId())) {
+ continue;
+ }
+ discovered++;
+ InstanceVO request = InstanceVO.builder()
+ .vendor(vendor)
+ .credentialId(credentialId)
+ .regionId(region.getRegionId())
+ .cloudInstanceId(option.getInstanceId())
+ .name(option.getInstanceId())
+ .build();
+ try {
+ createInstance(request);
+ imported++;
+ } catch (BusinessException ex) {
+ if (ex.getMessage() != null &&
ex.getMessage().startsWith("Instance name already exists")) {
+ skipped++;
+ } else {
+ failed.add(option.getInstanceId() + ": " +
ex.getMessage());
+ }
+ }
+ }
+ }
+ log.info("Cloud import finished: vendor={}, credentialId={},
discovered={}, imported={}, skipped={}, failed={}",
+ vendor, credentialId, discovered, imported, skipped,
failed.size());
+ recordAudit("IMPORT_CLOUD_INSTANCES", "INSTANCE",
String.valueOf(credentialId), null,
+ "vendor=" + vendor + ", imported=" + imported + ", skipped=" +
skipped
+ + ", failed=" + failed.size());
+ return CloudImportResultVO.builder()
+ .discovered(discovered)
+ .imported(imported)
+ .skipped(skipped)
+ .failed(failed)
+ .build();
+ }
+
private void requireUniqueInstanceName(String name, Long excludeId) {
if (!StringUtils.hasText(name)) {
return;
@@ -159,7 +234,9 @@ public class InstanceService {
if (instance.getType() == null) {
throw new BusinessException(400, "InstanceVO type is required");
}
- instance.setType(instance.getType().normalizeApacheType());
+ if (instance.getType() == InstanceType.CLOUD) {
+ throw new BusinessException(400, "CLOUD type is reserved for
vendor-managed instances");
+ }
}
/**
@@ -189,12 +266,14 @@ public class InstanceService {
"Cloud instance details unavailable: " +
instance.getCloudInstanceId());
}
if (!StringUtils.hasText(instance.getName())) {
- instance.setName(detail.getInstanceName() != null &&
!detail.getInstanceName().isBlank()
- ? detail.getInstanceName() : detail.getInstanceId());
+ instance.setName(detail.getInstanceId());
}
instance.setName(requireInstanceName(instance.getName()));
- instance.setType(InstanceType.PROXY);
+ instance.setType(InstanceType.CLOUD);
instance.setEndpoint(resolveEndpoint(detail));
+ if (!StringUtils.hasText(instance.getRemark()) &&
StringUtils.hasText(detail.getRemark())) {
+ instance.setRemark(detail.getRemark());
+ }
}
private String resolveEndpoint(CloudInstanceDetailVO detail) {
@@ -275,7 +354,10 @@ public class InstanceService {
}
if (!cloudInstance) {
if (instance.getType() != null) {
- updated.setType(instance.getType().normalizeApacheType());
+ if (instance.getType() == InstanceType.CLOUD) {
+ throw new BusinessException(400, "CLOUD type is reserved
for vendor-managed instances");
+ }
+ updated.setType(instance.getType());
}
if (instance.getEndpoint() != null) {
updated.setEndpoint(requireValidEndpoint(instance.getEndpoint()));
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
index 422e8643a..1452fbbf1 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
@@ -33,7 +33,6 @@ import
org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
-import java.util.stream.Stream;
@RequiredArgsConstructor
@Repository
@@ -55,7 +54,7 @@ public class MybatisPlusInstanceRepository implements
InstanceRepository {
public List<InstanceVO> findByType(InstanceType type) {
return instanceMapper.selectList(
new QueryWrapper<RmqInstance>()
- .in("type", typeNamesForFilter(type))
+ .eq("type", type.name())
.orderByAsc("id")).stream()
.map(this::toVO)
.toList();
@@ -77,7 +76,7 @@ public class MybatisPlusInstanceRepository implements
InstanceRepository {
public List<InstanceVO> findByTypeAndSearch(InstanceType type, String
keyword) {
return instanceMapper.selectList(
new QueryWrapper<RmqInstance>()
- .in("type", typeNamesForFilter(type))
+ .eq("type", type.name())
.and(w -> w.like("name", keyword)
.or().like("endpoint", keyword)
.or().like("remark", keyword))
@@ -86,16 +85,6 @@ public class MybatisPlusInstanceRepository implements
InstanceRepository {
.toList();
}
- private List<String> typeNamesForFilter(InstanceType type) {
- if (type == InstanceType.PROXY) {
- return Stream.of(InstanceType.values())
- .filter(InstanceType::isProxy)
- .map(Enum::name)
- .toList();
- }
- return List.of(type.name());
- }
-
@Override
public Optional<InstanceVO> findById(Long id) {
if (id == null) {
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
index 08fd392b7..902b760c8 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
@@ -334,7 +334,7 @@ public class RocketMQDashboardProvider implements
DashboardProvider {
return switch (instance.getType()) {
case DIRECT -> ClusterType.V4_DIRECT;
case PROXY_LOCAL -> ClusterType.V5_PROXY_LOCAL;
- case PROXY, PROXY_CLUSTER -> ClusterType.V5_PROXY_CLUSTER;
+ case CLOUD, PROXY_CLUSTER -> ClusterType.V5_PROXY_CLUSTER;
};
}
diff --git a/server/src/main/resources/db/schema.sql
b/server/src/main/resources/db/schema.sql
index 74063aa21..8f2a67eed 100644
--- a/server/src/main/resources/db/schema.sql
+++ b/server/src/main/resources/db/schema.sql
@@ -63,7 +63,7 @@ CREATE TABLE IF NOT EXISTS rmq_instance (
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '修改时间',
name VARCHAR(128) NOT NULL,
remark VARCHAR(255),
- type VARCHAR(32) NOT NULL COMMENT 'PROXY/PROXY_LOCAL/PROXY_CLUSTER/DIRECT',
+ type VARCHAR(32) NOT NULL COMMENT 'CLOUD/PROXY_LOCAL/PROXY_CLUSTER/DIRECT',
endpoint VARCHAR(512) NOT NULL,
vendor VARCHAR(32),
cloud_instance_id VARCHAR(128),
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceCapabilityServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceCapabilityServiceTest.java
index 428580186..b22cfdfd4 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceCapabilityServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceCapabilityServiceTest.java
@@ -57,7 +57,7 @@ class InstanceCapabilityServiceTest {
InstanceVO instance = InstanceVO.builder()
.name("cloud-1")
.vendor(InstanceVendor.ALIYUN)
- .type(InstanceType.PROXY)
+ .type(InstanceType.CLOUD)
.build();
instance.setId(1L);
when(instanceRepository.findById(1L)).thenReturn(Optional.of(instance));
@@ -70,7 +70,7 @@ class InstanceCapabilityServiceTest {
assertThat(result.instanceId()).isEqualTo("cloud-1");
assertThat(result.vendor()).isEqualTo(InstanceVendor.ALIYUN);
- assertThat(result.accessType()).isEqualTo(InstanceType.PROXY);
+ assertThat(result.accessType()).isEqualTo(InstanceType.CLOUD);
assertThat(result.capabilities()).containsExactly(
InstanceCapability.TOPIC_MANAGEMENT,
InstanceCapability.MESSAGE_QUERY);
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
index 2c87ce067..689494985 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
@@ -65,7 +65,7 @@ class InstanceControllerTest {
@Test
void listInstancesShouldReturnAllInstances() throws Exception {
- InstanceVO inst = buildInstance(1L, "production-proxy",
InstanceType.PROXY, "10.0.1.1:8080");
+ InstanceVO inst = buildInstance(1L, "production-proxy",
InstanceType.PROXY_CLUSTER, "10.0.1.1:8080");
when(instanceService.listInstances(isNull(),
isNull())).thenReturn(List.of(inst));
@@ -75,28 +75,28 @@ class InstanceControllerTest {
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data[0].id").value(1))
.andExpect(jsonPath("$.data[0].name").value("production-proxy"))
- .andExpect(jsonPath("$.data[0].type").value("PROXY"))
+ .andExpect(jsonPath("$.data[0].type").value("PROXY_CLUSTER"))
.andExpect(jsonPath("$.data[0].endpoint").value("10.0.1.1:8080"));
}
@Test
void listInstancesShouldFilterByType() throws Exception {
- InstanceVO inst = buildInstance(1L, "proxy-1", InstanceType.PROXY,
"10.0.1.1:8080");
+ InstanceVO inst = buildInstance(1L, "proxy-1", InstanceType.CLOUD,
"10.0.1.1:8080");
- when(instanceService.listInstances(eq(InstanceType.PROXY),
isNull())).thenReturn(List.of(inst));
+ when(instanceService.listInstances(eq(InstanceType.CLOUD),
isNull())).thenReturn(List.of(inst));
mockMvc.perform(get("/api/instances")
- .param("type", "PROXY"))
+ .param("type", "CLOUD"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray())
- .andExpect(jsonPath("$.data[0].type").value("PROXY"));
+ .andExpect(jsonPath("$.data[0].type").value("CLOUD"));
- verify(instanceService).listInstances(eq(InstanceType.PROXY),
isNull());
+ verify(instanceService).listInstances(eq(InstanceType.CLOUD),
isNull());
}
@Test
void listInstancesShouldFilterBySearch() throws Exception {
- InstanceVO inst = buildInstance(1L, "production", InstanceType.PROXY,
"10.0.1.1:8080");
+ InstanceVO inst = buildInstance(1L, "production",
InstanceType.PROXY_CLUSTER, "10.0.1.1:8080");
when(instanceService.listInstances(isNull(),
eq("prod"))).thenReturn(List.of(inst));
@@ -177,7 +177,7 @@ class InstanceControllerTest {
InstanceVO updated = InstanceVO.builder()
.name("updated-name")
.endpoint("10.0.1.1:8080")
- .type(InstanceType.PROXY)
+ .type(InstanceType.PROXY_CLUSTER)
.build();
updated.setId(1L);
updated.setGmtCreate(LocalDateTime.of(2026, 1, 1, 0, 0));
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
index 5ce91ec65..0d5474f30 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
@@ -26,12 +26,15 @@ import
org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.apache.rocketmq.studio.provider.CloudCatalogProvider;
import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
+import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
+import org.apache.rocketmq.studio.provider.CloudRegionVO;
import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
import org.apache.rocketmq.studio.provider.InstanceProvider;
import org.apache.rocketmq.studio.settings.DataSourceVO;
import org.apache.rocketmq.studio.settings.SettingsRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -146,15 +149,15 @@ class InstanceServiceTest {
@Test
void listInstancesShouldFilterByType() {
List<InstanceVO> instances = List.of(
-
InstanceVO.builder().name("proxy-1").type(InstanceType.PROXY).build()
+
InstanceVO.builder().name("proxy-1").type(InstanceType.CLOUD).build()
);
-
when(instanceRepository.findByType(InstanceType.PROXY)).thenReturn(instances);
+
when(instanceRepository.findByType(InstanceType.CLOUD)).thenReturn(instances);
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
- List<InstanceVO> result =
instanceService.listInstances(InstanceType.PROXY, null);
+ List<InstanceVO> result =
instanceService.listInstances(InstanceType.CLOUD, null);
assertThat(result).hasSize(1);
- verify(instanceRepository).findByType(InstanceType.PROXY);
+ verify(instanceRepository).findByType(InstanceType.CLOUD);
}
@Test
@@ -188,29 +191,29 @@ class InstanceServiceTest {
@Test
void listInstancesShouldFilterByTypeAndSearch() {
List<InstanceVO> instances = List.of(
-
InstanceVO.builder().name("production-proxy").type(InstanceType.PROXY).build()
+
InstanceVO.builder().name("production-proxy").type(InstanceType.CLOUD).build()
);
- when(instanceRepository.findByTypeAndSearch(InstanceType.PROXY,
"prod")).thenReturn(instances);
+ when(instanceRepository.findByTypeAndSearch(InstanceType.CLOUD,
"prod")).thenReturn(instances);
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
- List<InstanceVO> result =
instanceService.listInstances(InstanceType.PROXY, "prod");
+ List<InstanceVO> result =
instanceService.listInstances(InstanceType.CLOUD, "prod");
assertThat(result).hasSize(1);
- verify(instanceRepository).findByTypeAndSearch(InstanceType.PROXY,
"prod");
+ verify(instanceRepository).findByTypeAndSearch(InstanceType.CLOUD,
"prod");
}
@Test
void listInstancesShouldTrimSearchKeywordWhenFilteringByType() {
List<InstanceVO> instances = List.of(
-
InstanceVO.builder().name("production-proxy").type(InstanceType.PROXY).build()
+
InstanceVO.builder().name("production-proxy").type(InstanceType.CLOUD).build()
);
- when(instanceRepository.findByTypeAndSearch(InstanceType.PROXY,
"prod")).thenReturn(instances);
+ when(instanceRepository.findByTypeAndSearch(InstanceType.CLOUD,
"prod")).thenReturn(instances);
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
- List<InstanceVO> result =
instanceService.listInstances(InstanceType.PROXY, " prod ");
+ List<InstanceVO> result =
instanceService.listInstances(InstanceType.CLOUD, " prod ");
assertThat(result).hasSize(1);
- verify(instanceRepository).findByTypeAndSearch(InstanceType.PROXY,
"prod");
+ verify(instanceRepository).findByTypeAndSearch(InstanceType.CLOUD,
"prod");
}
@Test
@@ -278,7 +281,7 @@ class InstanceServiceTest {
InstanceVO input = InstanceVO.builder()
.name("new-instance")
.endpoint("10.0.1.1:8080")
- .type(InstanceType.PROXY)
+ .type(InstanceType.PROXY_CLUSTER)
.build();
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation -> {
@@ -362,7 +365,7 @@ class InstanceServiceTest {
@Test
void createInstanceShouldTrimEndpointBeforeSaving() {
- InstanceVO input =
InstanceVO.builder().name("valid-name").type(InstanceType.PROXY)
+ InstanceVO input =
InstanceVO.builder().name("valid-name").type(InstanceType.PROXY_CLUSTER)
.endpoint(" namesrv:9876 ").build();
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
@@ -371,7 +374,7 @@ class InstanceServiceTest {
@Test
void createInstanceShouldTrimNameBeforeSaving() {
- InstanceVO input = InstanceVO.builder().name(" production
").type(InstanceType.PROXY)
+ InstanceVO input = InstanceVO.builder().name(" production
").type(InstanceType.PROXY_CLUSTER)
.endpoint("namesrv:9876").build();
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
@@ -380,7 +383,7 @@ class InstanceServiceTest {
@Test
void
createApacheInstanceShouldTrimAndPersistOnlyAdminCredentialReference() {
- InstanceVO input =
InstanceVO.builder().name("production").type(InstanceType.PROXY)
+ InstanceVO input =
InstanceVO.builder().name("production").type(InstanceType.PROXY_CLUSTER)
.endpoint("namesrv:9876").adminCredentialRef("
production-admin ").build();
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
@@ -391,7 +394,7 @@ class InstanceServiceTest {
@Test
void
updateApacheInstanceShouldReleaseCachedClientWhenCredentialReferenceChanges() {
- InstanceVO existing =
InstanceVO.builder().name("production").type(InstanceType.PROXY)
+ InstanceVO existing =
InstanceVO.builder().name("production").type(InstanceType.PROXY_CLUSTER)
.endpoint("namesrv:9876").adminCredentialRef("credential-a").build();
existing.setId(1L);
InstanceVO update =
InstanceVO.builder().adminCredentialRef("credential-b").build();
@@ -412,7 +415,7 @@ class InstanceServiceTest {
InstanceVO existing = InstanceVO.builder()
.name("old-name")
.endpoint("10.0.1.1:8080")
- .type(InstanceType.PROXY)
+ .type(InstanceType.PROXY_CLUSTER)
.remark("old remark")
.topicCount(7)
.consumerGroupCount(3)
@@ -434,7 +437,7 @@ class InstanceServiceTest {
assertThat(result.getName()).isEqualTo("old-name");
assertThat(result.getEndpoint()).isEqualTo("10.0.1.1:8080");
- assertThat(result.getType()).isEqualTo(InstanceType.PROXY);
+ assertThat(result.getType()).isEqualTo(InstanceType.PROXY_CLUSTER);
assertThat(result.getRemark()).isEqualTo("new remark");
assertThat(result.getTopicCount()).isEqualTo(7);
assertThat(result.getConsumerGroupCount()).isEqualTo(3);
@@ -446,7 +449,7 @@ class InstanceServiceTest {
assertThat(existing.getRemark()).isEqualTo("old remark");
assertThat(existing.getGmtModified()).isEqualTo(originalUpdatedAt);
verify(operationAuditService).record(eq("UPDATE_INSTANCE"),
eq("INSTANCE"), eq("1"), eq(null),
- eq("name=old-name, vendor=APACHE, type=PROXY"), eq("SUCCESS"),
eq(null));
+ eq("name=old-name, vendor=APACHE, type=PROXY_CLUSTER"),
eq("SUCCESS"), eq(null));
}
@Test
@@ -470,7 +473,7 @@ class InstanceServiceTest {
InstanceVO stored = InstanceVO.builder()
.name("old-name")
.remark("old remark")
- .type(InstanceType.PROXY)
+ .type(InstanceType.PROXY_CLUSTER)
.endpoint("10.0.1.1:8080")
.topicCount(7)
.consumerGroupCount(3)
@@ -497,7 +500,7 @@ class InstanceServiceTest {
assertThat(stored.getName()).isEqualTo("old-name");
assertThat(stored.getRemark()).isEqualTo("old remark");
- assertThat(stored.getType()).isEqualTo(InstanceType.PROXY);
+ assertThat(stored.getType()).isEqualTo(InstanceType.PROXY_CLUSTER);
assertThat(stored.getEndpoint()).isEqualTo("10.0.1.1:8080");
assertThat(stored.getTopicCount()).isEqualTo(7);
assertThat(stored.getConsumerGroupCount()).isEqualTo(3);
@@ -817,7 +820,7 @@ class InstanceServiceTest {
InstanceVO instance = InstanceVO.builder()
.name("inst")
.endpoint("10.0.0.1:8080")
- .type(InstanceType.PROXY)
+ .type(InstanceType.PROXY_CLUSTER)
.build();
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
@@ -857,6 +860,39 @@ class InstanceServiceTest {
verify(instanceRepository, never()).save(any(InstanceVO.class));
}
+ @Test
+ void createApacheInstanceShouldRejectCloudTypeTest() {
+ InstanceVO instance = InstanceVO.builder()
+ .name("inst")
+ .endpoint("10.0.0.1:8080")
+ .type(InstanceType.CLOUD)
+ .build();
+
+ assertThatThrownBy(() -> instanceService.createInstance(instance))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("CLOUD type is reserved for vendor-managed
instances")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(400));
+
+ verify(instanceRepository, never()).save(any(InstanceVO.class));
+ }
+
+ @Test
+ void updateApacheInstanceShouldRejectCloudTypeTest() {
+ InstanceVO existing =
InstanceVO.builder().name("production").type(InstanceType.PROXY_CLUSTER)
+ .endpoint("namesrv:9876").build();
+ existing.setId(1L);
+ InstanceVO update =
InstanceVO.builder().type(InstanceType.CLOUD).build();
+ update.setId(1L);
+
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
+
+ assertThatThrownBy(() -> instanceService.updateInstance(update))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("CLOUD type is reserved for vendor-managed
instances")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(400));
+
+ verify(instanceRepository, never()).save(any(InstanceVO.class));
+ }
+
@Test
void createInstanceShouldRejectManualEndpointForAliyunTest() {
InstanceVO instance = InstanceVO.builder()
@@ -926,9 +962,131 @@ class InstanceServiceTest {
InstanceVO created = instanceService.createInstance(instance);
- assertThat(created.getName()).isEqualTo("prod-mq");
+ assertThat(created.getName()).isEqualTo("rmq-cn-xxx");
assertThat(created.getEndpoint()).isEqualTo("vpc:8080");
- assertThat(created.getType()).isEqualTo(InstanceType.PROXY);
+ assertThat(created.getType()).isEqualTo(InstanceType.CLOUD);
+ }
+
+ @Test
+ void createInstanceShouldAutoFillRemarkFromCloudDetailTest() {
+ InstanceVO instance = InstanceVO.builder()
+ .vendor(InstanceVendor.ALIYUN)
+ .credentialId(1L)
+ .cloudInstanceId("rmq-cn-xxx")
+ .regionId("cn-hangzhou")
+ .build();
+ CloudCredentialVO credential = new CloudCredentialVO();
+ credential.setId(1L);
+ credential.setVendor(InstanceVendor.ALIYUN);
+
when(cloudCredentialRepository.findById(1L)).thenReturn(Optional.of(credential));
+ CloudCatalogProvider catalog =
org.mockito.Mockito.mock(CloudCatalogProvider.class);
+ CloudInstanceDetailVO detail = new CloudInstanceDetailVO();
+ detail.setInstanceId("rmq-cn-xxx");
+ detail.setInstanceName("prod-mq");
+ detail.setRemark("prod-link");
+ detail.setEndpoints(List.of(new
CloudInstanceDetailVO.CloudEndpoint("TCP_VPC", "vpc:8080")));
+
when(providerRegistry.catalogFor(InstanceVendor.ALIYUN)).thenReturn(catalog);
+ when(catalog.getCloudInstance(1L, "cn-hangzhou",
"rmq-cn-xxx")).thenReturn(detail);
+
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
+
+ InstanceVO created = instanceService.createInstance(instance);
+
+ assertThat(created.getRemark()).isEqualTo("prod-link");
+ }
+
+ @Test
+ void createInstanceShouldKeepExplicitRemarkOverCloudDetailTest() {
+ InstanceVO instance = InstanceVO.builder()
+ .vendor(InstanceVendor.ALIYUN)
+ .credentialId(1L)
+ .cloudInstanceId("rmq-cn-xxx")
+ .regionId("cn-hangzhou")
+ .remark("user-remark")
+ .build();
+ CloudCredentialVO credential = new CloudCredentialVO();
+ credential.setId(1L);
+ credential.setVendor(InstanceVendor.ALIYUN);
+
when(cloudCredentialRepository.findById(1L)).thenReturn(Optional.of(credential));
+ CloudCatalogProvider catalog =
org.mockito.Mockito.mock(CloudCatalogProvider.class);
+ CloudInstanceDetailVO detail = new CloudInstanceDetailVO();
+ detail.setInstanceId("rmq-cn-xxx");
+ detail.setInstanceName("prod-mq");
+ detail.setRemark("cloud-remark");
+ detail.setEndpoints(List.of(new
CloudInstanceDetailVO.CloudEndpoint("TCP_VPC", "vpc:8080")));
+
when(providerRegistry.catalogFor(InstanceVendor.ALIYUN)).thenReturn(catalog);
+ when(catalog.getCloudInstance(1L, "cn-hangzhou",
"rmq-cn-xxx")).thenReturn(detail);
+
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
+
+ InstanceVO created = instanceService.createInstance(instance);
+
+ assertThat(created.getRemark()).isEqualTo("user-remark");
+ }
+
+ @Test
+ void importCloudInstancesShouldRejectApacheVendorTest() {
+ assertThatThrownBy(() ->
instanceService.importCloudInstances(InstanceVendor.APACHE, 1L))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Import is only supported for cloud vendors")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(400));
+
+ verifyNoInteractions(instanceRepository);
+ }
+
+ @Test
+ void importCloudInstancesShouldImportSkipAndCollectFailuresTest() {
+ CloudCredentialVO credential = new CloudCredentialVO();
+ credential.setId(1L);
+ credential.setVendor(InstanceVendor.ALIYUN);
+
when(cloudCredentialRepository.findById(1L)).thenReturn(Optional.of(credential));
+
+ CloudCatalogProvider catalog =
org.mockito.Mockito.mock(CloudCatalogProvider.class);
+
when(providerRegistry.catalogFor(InstanceVendor.ALIYUN)).thenReturn(catalog);
+
+ CloudRegionVO hangzhou = new CloudRegionVO();
+ hangzhou.setRegionId("cn-hangzhou");
+ CloudRegionVO broken = new CloudRegionVO();
+ broken.setRegionId("cn-broken");
+ when(catalog.listRegions(1L)).thenReturn(List.of(hangzhou, broken));
+
+ CloudInstanceOptionVO fresh = new CloudInstanceOptionVO();
+ fresh.setInstanceId("rmq-fresh");
+ CloudInstanceOptionVO duplicate = new CloudInstanceOptionVO();
+ duplicate.setInstanceId("rmq-dup");
+ when(catalog.listCloudInstances(1L, "cn-hangzhou",
null)).thenReturn(List.of(fresh, duplicate));
+ when(catalog.listCloudInstances(1L, "cn-broken", null))
+ .thenThrow(new BusinessException(502, "catalog unavailable"));
+
+ CloudInstanceDetailVO freshDetail = new CloudInstanceDetailVO();
+ freshDetail.setInstanceId("rmq-fresh");
+ freshDetail.setInstanceName("inst-fresh");
+ freshDetail.setRemark("cloud-remark");
+ freshDetail.setEndpoints(List.of(new
CloudInstanceDetailVO.CloudEndpoint("TCP_VPC", "vpc-a:8080")));
+ CloudInstanceDetailVO duplicateDetail = new CloudInstanceDetailVO();
+ duplicateDetail.setInstanceId("rmq-dup");
+ duplicateDetail.setInstanceName("inst-dup");
+ duplicateDetail.setEndpoints(List.of(new
CloudInstanceDetailVO.CloudEndpoint("TCP_VPC", "vpc-b:8080")));
+ when(catalog.getCloudInstance(1L, "cn-hangzhou",
"rmq-fresh")).thenReturn(freshDetail);
+ when(catalog.getCloudInstance(1L, "cn-hangzhou",
"rmq-dup")).thenReturn(duplicateDetail);
+
+
when(instanceRepository.findByName("rmq-fresh")).thenReturn(Optional.empty());
+ InstanceVO existing = InstanceVO.builder().name("rmq-dup").build();
+ existing.setId(9L);
+
when(instanceRepository.findByName("rmq-dup")).thenReturn(Optional.of(existing));
+
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
+
+ CloudImportResultVO result =
instanceService.importCloudInstances(InstanceVendor.ALIYUN, 1L);
+
+ assertThat(result.getDiscovered()).isEqualTo(2);
+ assertThat(result.getImported()).isEqualTo(1);
+ assertThat(result.getSkipped()).isEqualTo(1);
+ assertThat(result.getFailed()).containsExactly("cn-broken: catalog
unavailable");
+
+ ArgumentCaptor<InstanceVO> saved =
ArgumentCaptor.forClass(InstanceVO.class);
+ verify(instanceRepository).save(saved.capture());
+ assertThat(saved.getValue().getName()).isEqualTo("rmq-fresh");
+ assertThat(saved.getValue().getRemark()).isEqualTo("cloud-remark");
+ assertThat(saved.getValue().getType()).isEqualTo(InstanceType.CLOUD);
+ assertThat(saved.getValue().getRegionId()).isEqualTo("cn-hangzhou");
}
@Test
@@ -1015,9 +1173,9 @@ class InstanceServiceTest {
InstanceVO created = instanceService.createInstance(instance);
- assertThat(created.getName()).isEqualTo("chengdu-prod");
+ assertThat(created.getName()).isEqualTo("rmq-abc");
assertThat(created.getEndpoint()).isEqualTo("vpc.tencent:8080");
- assertThat(created.getType()).isEqualTo(InstanceType.PROXY);
+ assertThat(created.getType()).isEqualTo(InstanceType.CLOUD);
assertThat(created.getVendor()).isEqualTo(InstanceVendor.TENCENT);
}
@@ -1029,7 +1187,7 @@ class InstanceServiceTest {
.cloudInstanceId("rmq-cn-xxx")
.credentialId(1L)
.regionId("cn-hangzhou")
- .type(InstanceType.PROXY)
+ .type(InstanceType.CLOUD)
.endpoint("vpc:8080")
.build();
existing.setId(1L);
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
index ab559c1ee..3aa83c2fa 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
@@ -63,7 +63,7 @@ class MybatisPlusInstanceRepositoryTest {
void findAllShouldMapEntitiesWithoutCountsTest() {
when(instanceMapper.selectList(any(QueryWrapper.class)))
.thenReturn(List.of(entity(1L, "instance-direct-1",
InstanceType.DIRECT),
- entity(2L, "instance-proxy-1", InstanceType.PROXY)));
+ entity(2L, "instance-proxy-1",
InstanceType.PROXY_CLUSTER)));
List<InstanceVO> result = repository.findAll();
@@ -81,22 +81,19 @@ class MybatisPlusInstanceRepositoryTest {
}
@Test
- void findByTypeShouldTreatLegacyProxyAsAllProxyAccessTypes() {
+ void findByTypeShouldMatchExactTypeNameTest() {
when(instanceMapper.selectList(any(QueryWrapper.class)))
- .thenReturn(List.of(
- entity(3L, "legacy", InstanceType.PROXY),
- entity(4L, "local", InstanceType.PROXY_LOCAL),
- entity(5L, "cluster", InstanceType.PROXY_CLUSTER)));
+ .thenReturn(List.of(entity(3L, "cloud-inst",
InstanceType.CLOUD)));
- List<InstanceVO> result = repository.findByType(InstanceType.PROXY);
+ List<InstanceVO> result = repository.findByType(InstanceType.CLOUD);
assertThat(result).extracting(InstanceVO::getType)
- .containsExactly(InstanceType.PROXY, InstanceType.PROXY_LOCAL,
InstanceType.PROXY_CLUSTER);
+ .containsExactly(InstanceType.CLOUD);
ArgumentCaptor<QueryWrapper<RmqInstance>> query =
ArgumentCaptor.forClass(QueryWrapper.class);
verify(instanceMapper).selectList(query.capture());
- assertThat(query.getValue().getSqlSegment()).contains("type IN");
+ assertThat(query.getValue().getSqlSegment()).contains("type =");
assertThat(query.getValue().getParamNameValuePairs().values())
- .contains(InstanceType.PROXY.name(),
InstanceType.PROXY_LOCAL.name(), InstanceType.PROXY_CLUSTER.name());
+ .containsExactly(InstanceType.CLOUD.name());
}
@Test
@@ -109,7 +106,7 @@ class MybatisPlusInstanceRepositoryTest {
.isEqualTo(InstanceType.PROXY_LOCAL);
ArgumentCaptor<QueryWrapper<RmqInstance>> query =
ArgumentCaptor.forClass(QueryWrapper.class);
verify(instanceMapper).selectList(query.capture());
- assertThat(query.getValue().getSqlSegment()).contains("type IN");
+ assertThat(query.getValue().getSqlSegment()).contains("type =");
}
@Test
@@ -137,7 +134,7 @@ class MybatisPlusInstanceRepositoryTest {
@Test
void findByIdShouldNotComputeCountsTest() {
- when(instanceMapper.selectById(2L)).thenReturn(entity(2L,
"instance-proxy-1", InstanceType.PROXY));
+ when(instanceMapper.selectById(2L)).thenReturn(entity(2L,
"instance-proxy-1", InstanceType.PROXY_CLUSTER));
Optional<InstanceVO> result = repository.findById(2L);
@@ -189,7 +186,7 @@ class MybatisPlusInstanceRepositoryTest {
@Test
void saveShouldInsertWhenInstanceAbsent() {
- InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY);
+ InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY_CLUSTER);
when(instanceMapper.selectById(5L)).thenReturn(null);
repository.save(vo);
@@ -202,8 +199,8 @@ class MybatisPlusInstanceRepositoryTest {
@Test
void saveShouldUpdateWhenInstanceExists() {
- InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY);
- when(instanceMapper.selectById(5L)).thenReturn(entity(5L,
"instance-proxy-2", InstanceType.PROXY));
+ InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY_CLUSTER);
+ when(instanceMapper.selectById(5L)).thenReturn(entity(5L,
"instance-proxy-2", InstanceType.PROXY_CLUSTER));
when(instanceMapper.updateById(any(RmqInstance.class))).thenReturn(1);
repository.save(vo);
@@ -214,9 +211,9 @@ class MybatisPlusInstanceRepositoryTest {
@Test
void saveShouldReportALostConcurrentUpdate() {
- InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY);
+ InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY_CLUSTER);
when(instanceMapper.selectById(5L))
- .thenReturn(entity(5L, "instance-proxy-2",
InstanceType.PROXY));
+ .thenReturn(entity(5L, "instance-proxy-2",
InstanceType.PROXY_CLUSTER));
when(instanceMapper.updateById(any(RmqInstance.class))).thenReturn(0);
assertThatThrownBy(() -> repository.save(vo))
diff --git a/web/src/api/instance.test.ts b/web/src/api/instance.test.ts
index 7998d6dc7..b06d902c3 100644
--- a/web/src/api/instance.test.ts
+++ b/web/src/api/instance.test.ts
@@ -31,7 +31,7 @@ const mock = new MockAdapter(client);
const instance = {
id: 1,
name: 'orders',
- type: 'PROXY' as const,
+ type: 'PROXY_CLUSTER' as const,
endpoint: 'proxy:8080',
remark: '',
topicCount: 0,
@@ -67,11 +67,11 @@ describe('instance API', () => {
it('sends normalized instance filters', async () => {
mock.onGet('/instances').reply((config) => {
- expect(config.params).toEqual({ type: 'PROXY', search: 'proxy:8080' });
+ expect(config.params).toEqual({ type: 'CLOUD', search: 'proxy:8080' });
return [200, { code: 200, data: [instance] }];
});
- await expect(listInstances({ type: 'PROXY', search: ' proxy:8080 '
})).resolves.toEqual([
+ await expect(listInstances({ type: 'CLOUD', search: ' proxy:8080 '
})).resolves.toEqual([
instance,
]);
});
@@ -98,7 +98,7 @@ describe('instance API', () => {
const capabilities = {
instanceId: 'instance/proxy',
vendor: 'APACHE' as const,
- accessType: 'PROXY' as const,
+ accessType: 'PROXY_CLUSTER' as const,
capabilities: ['TOPIC_MANAGEMENT', 'DLQ_MANAGEMENT'] as const,
};
mock.onGet('/instances/instance%2Fproxy/capabilities').reply(200, {
diff --git a/web/src/api/instance.ts b/web/src/api/instance.ts
index d61e03c1c..853b923d9 100644
--- a/web/src/api/instance.ts
+++ b/web/src/api/instance.ts
@@ -19,7 +19,7 @@ import client from './client';
// ─── Types ──────────────────────────────────────────────────────
export type InstanceVendor = 'APACHE' | 'ALIYUN' | 'TENCENT';
-export type InstanceType = 'PROXY' | 'PROXY_LOCAL' | 'PROXY_CLUSTER' |
'DIRECT';
+export type InstanceType = 'CLOUD' | 'PROXY_LOCAL' | 'PROXY_CLUSTER' |
'DIRECT';
export type InstanceCapability =
| 'TOPIC_MANAGEMENT'
| 'CONSUMER_GROUP_MANAGEMENT'
@@ -112,6 +112,18 @@ export async function updateInstance(data:
UpdateInstanceRequest) {
return res.data.data;
}
+export interface CloudImportResult {
+ discovered: number;
+ imported: number;
+ skipped: number;
+ failed: string[];
+}
+
export async function deleteInstance(instanceId: string) {
await client.post('/instances/delete', { id: instanceId });
}
+
+export async function importCloudInstances(data: { vendor: InstanceVendor;
credentialId: number }) {
+ const res = await client.post<{ data: CloudImportResult
}>('/instances/import-cloud', data);
+ return res.data.data;
+}
diff --git a/web/src/hooks/useInstanceFilter.test.tsx
b/web/src/hooks/useInstanceFilter.test.tsx
index f3722eb8a..461b7f0e1 100644
--- a/web/src/hooks/useInstanceFilter.test.tsx
+++ b/web/src/hooks/useInstanceFilter.test.tsx
@@ -39,7 +39,7 @@ describe('useInstanceFilter', () => {
id: 7,
name: 'instance-a',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '127.0.0.1:8080',
topicCount: 0,
consumerGroupCount: 0,
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index e77f6a69c..648e2ae88 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -179,7 +179,6 @@ const translations: Record<string, Record<Lang, string>> = {
'instance.count': { zh: '共 {n} 个实例', en: '{n} instances' },
'instance.searchPlaceholder': { zh: '搜索实例 ID 或地址', en: 'Search instance ID
or endpoint' },
'instance.allTypes': { zh: '全部架构', en: 'All Types' },
- 'instance.proxyMode': { zh: 'Proxy 模式', en: 'Proxy Mode' },
'instance.directMode': { zh: 'Direct 模式', en: 'Direct Mode' },
'instance.addInstance': { zh: '添加实例', en: 'Add Instance' },
'instance.instanceName': { zh: '实例 ID', en: 'Instance ID' },
diff --git a/web/src/layouts/MainLayout.test.tsx
b/web/src/layouts/MainLayout.test.tsx
index 3a9b3b7dc..fbf7844ea 100644
--- a/web/src/layouts/MainLayout.test.tsx
+++ b/web/src/layouts/MainLayout.test.tsx
@@ -175,7 +175,7 @@ describe('MainLayout authentication navigation', () => {
instanceServiceMocks.getInstanceCapabilities.mockResolvedValue({
instanceId: 'cloud-1',
vendor: 'ALIYUN',
- accessType: 'PROXY',
+ accessType: 'CLOUD',
capabilities: ['TOPIC_MANAGEMENT'],
});
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 68f908204..d28399a1d 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -112,7 +112,7 @@ describe('Consumer page', () => {
id: 1,
name: 'instance-1',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '10.0.0.1:8080',
topicCount: 0,
consumerGroupCount: 0,
@@ -176,7 +176,7 @@ describe('Consumer page', () => {
id: 1,
name: 'instance-1',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '10.0.0.1:8080',
topicCount: 0,
consumerGroupCount: 0,
@@ -649,7 +649,7 @@ describe('Consumer page', () => {
id: 4,
name: 'instance-proxy-1',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '10.0.2.21:8080',
topicCount: 0,
consumerGroupCount: 0,
diff --git a/web/src/pages/instance/__tests__/InstancePage.test.tsx
b/web/src/pages/instance/__tests__/InstancePage.test.tsx
index a2f60a103..51ac7a8ad 100644
--- a/web/src/pages/instance/__tests__/InstancePage.test.tsx
+++ b/web/src/pages/instance/__tests__/InstancePage.test.tsx
@@ -45,6 +45,7 @@ vi.mock('../../../api/tencentCatalog', () => ({
vi.mock('../../../services/instanceService', () => ({
createInstance: vi.fn(),
deleteInstance: vi.fn(),
+ importCloudInstances: vi.fn(),
listInstances: vi.fn(),
updateInstance: vi.fn(),
}));
@@ -75,7 +76,7 @@ beforeAll(() => {
const instance = (
id: number,
name: string,
- type: Instance['type'] = 'PROXY',
+ type: Instance['type'] = 'PROXY_CLUSTER',
remark: Instance['remark'] = '',
): Instance => ({
id,
@@ -398,6 +399,56 @@ describe('InstancePage', () => {
expect(within(dialog).getByText(/云凭据与云上实例完成接入/)).toBeInTheDocument();
});
+ it('imports every Aliyun instance of the credential via one-click import',
async () => {
+ const user = userEvent.setup();
+ vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(
+ cloudCredentialPage([
+ {
+ id: 101,
+ name: 'prod-account',
+ vendor: 'ALIYUN',
+ accessKey: 'LTAI-prod',
+ gmtCreate: '2026-01-01T00:00:00Z',
+ },
+ ]),
+ );
+ vi.mocked(instanceService.importCloudInstances).mockResolvedValue({
+ discovered: 3,
+ imported: 2,
+ skipped: 1,
+ failed: [],
+ });
+
+ renderPage();
+ expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: /添加实例/ }));
+ const dialog = await screen.findByRole('dialog');
+ await user.click(within(dialog).getByRole('tab', { name: /Aliyun 版/ }));
+ await waitFor(() =>
expect(cloudCredentialApi.listCloudCredentials).toHaveBeenCalled());
+
+ const importButton = within(dialog).getByRole('button', { name: /一键导入/ });
+ expect(importButton).toBeDisabled();
+
+ const credentialSelect = within(dialog).getAllByRole('combobox')[0];
+ fireEvent.mouseDown(credentialSelect.parentElement!);
+ await user.click(
+ await screen.findByText(/prod-account/, { selector:
'.ant-select-item-option-content' }),
+ );
+ await waitFor(() => expect(importButton).toBeEnabled());
+
+ await user.click(importButton);
+
+ await waitFor(() =>
+ expect(instanceService.importCloudInstances).toHaveBeenCalledWith({
+ vendor: 'ALIYUN',
+ credentialId: 101,
+ }),
+ );
+ expect(
+ await screen.findByText(/导入完成:共同步 3 个实例(新导入 2,已存在跳过 1)/),
+ ).toBeInTheDocument();
+ });
+
it('ignores a stale region response after the cloud credential changes',
async () => {
const user = userEvent.setup();
const oldRegions = deferred<Array<{ regionId: string; regionName: string
}>>();
@@ -687,8 +738,8 @@ describe('InstancePage', () => {
it('sorts and renders instances without remarks', async () => {
const user = userEvent.setup();
vi.mocked(instanceService.listInstances).mockResolvedValue([
- instance(10, 'instance-without-remark', 'PROXY', null),
- instance(11, 'instance-with-remark', 'PROXY', 'production'),
+ instance(10, 'instance-without-remark', 'PROXY_CLUSTER', null),
+ instance(11, 'instance-with-remark', 'PROXY_CLUSTER', 'production'),
]);
renderPage();
diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index 6a61755f1..7c97b2009 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -101,7 +101,7 @@ const selectedInstance = {
id: 5,
name: 'instance-proxy-1',
remark: '',
- type: 'PROXY' as const,
+ type: 'PROXY_CLUSTER' as const,
endpoint: '10.0.2.21:8080',
topicCount: 0,
consumerGroupCount: 0,
@@ -157,7 +157,7 @@ describe('TopicPage', () => {
id: 5,
name: 'instance-proxy-1',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '10.0.2.21:8080',
topicCount: 1,
consumerGroupCount: 0,
@@ -258,9 +258,7 @@ describe('TopicPage', () => {
gmtModified: '2026-01-01T00:00:00Z',
},
]);
- mockTopicsList(
- buildTopics(25).map((topic) => ({ ...topic, instanceId: 'instance-a' })),
- );
+ mockTopicsList(buildTopics(25).map((topic) => ({ ...topic, instanceId:
'instance-a' })));
renderWithProviders('/instance/instance-a/topic');
expect(await screen.findByText('topic-01')).toBeInTheDocument();
@@ -355,7 +353,7 @@ describe('TopicPage', () => {
id: 5,
name: 'instance-proxy-1',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '10.0.2.21:8080',
topicCount: 1,
consumerGroupCount: 0,
@@ -366,7 +364,7 @@ describe('TopicPage', () => {
id: 7,
name: 'instance-proxy-2',
remark: '',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: '10.0.2.22:8080',
topicCount: 1,
consumerGroupCount: 0,
@@ -415,9 +413,7 @@ describe('TopicPage', () => {
it('does not call createTopic when imported topic CSV is invalid or
duplicated', async () => {
const user = userEvent.setup();
instanceServiceMocks.listInstances.mockResolvedValue([selectedInstance]);
- mockTopicsList([
- { ...buildTopics(1)[0], instanceId: 'instance-proxy-1' },
- ]);
+ mockTopicsList([{ ...buildTopics(1)[0], instanceId: 'instance-proxy-1' }]);
renderWithProviders('/instance/instance-proxy-1/topic');
expect(await screen.findByText('topic-01')).toBeInTheDocument();
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 547f5a61e..0d74e93ff 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -51,6 +51,7 @@ import { formatDateTime } from '../../utils/format';
import {
createInstance,
deleteInstance,
+ importCloudInstances,
listInstances,
updateInstance,
} from '../../services/instanceService';
@@ -65,7 +66,7 @@ const DEFAULT_CLOUD_REGION_IDS:
Partial<Record<InstanceVendor, string>> = {
/* ─── Helpers ─── */
const typeLabel: Record<string, { text: string; color: string }> = {
- PROXY: { text: 'Proxy 模式(部署形态未标明)', color: 'blue' },
+ CLOUD: { text: '云服务', color: 'blue' },
PROXY_LOCAL: { text: 'Proxy Local 模式', color: 'cyan' },
PROXY_CLUSTER: { text: 'Proxy Cluster 模式', color: 'blue' },
DIRECT: { text: 'Direct 模式', color: 'orange' },
@@ -125,6 +126,7 @@ const InstancePage = () => {
const [editForm] = Form.useForm();
const editInstanceType = Form.useWatch<Instance['type'] | undefined>('type',
editForm);
const [submitting, setSubmitting] = useState(false);
+ const [importing, setImporting] = useState(false);
const requestIdRef = useRef(0);
const mutationInFlightRef = useRef(false);
const listQueryRef = useRef<InstanceQuery>({});
@@ -339,6 +341,38 @@ const InstancePage = () => {
}
};
+ const handleImportAll = async () => {
+ if (importing || vendor === 'APACHE') return;
+ const credentialId = addForm.getFieldValue('credentialId') as number |
undefined;
+ if (!credentialId) {
+ message.warning('请先选择云凭据');
+ return;
+ }
+ setImporting(true);
+ try {
+ const result = await importCloudInstances({ vendor, credentialId });
+ await loadInstances();
+ const summary =
+ result.imported > 0
+ ? `导入完成:共同步 ${result.imported + result.skipped} 个实例(新导入
${result.imported},已存在跳过 ${result.skipped})`
+ : `云上实例均已在 Studio 中(共 ${result.skipped} 个),无需重复导入`;
+ if (result.failed.length > 0) {
+ message.warning(`${summary},失败 ${result.failed.length}
个:${result.failed.join(';')}`);
+ } else {
+ message.success(summary);
+ }
+ setAddModalOpen(false);
+ addForm.resetFields();
+ setVendor(DEFAULT_VENDOR);
+ setRegions([]);
+ setCloudInstances([]);
+ } catch (error) {
+ message.error(describeApiError(error, '一键导入失败,请稍后重试'));
+ } finally {
+ setImporting(false);
+ }
+ };
+
const handleUpdate = async () => {
if (!editingInstance || mutationInFlightRef.current) return;
mutationInFlightRef.current = true;
@@ -569,7 +603,7 @@ const InstancePage = () => {
style={{ width: 140 }}
options={[
{ value: 'ALL', label: '全部架构' },
- { value: 'PROXY', label: '全部 Proxy 模式' },
+ { value: 'CLOUD', label: '云服务' },
{ value: 'PROXY_LOCAL', label: 'Proxy Local 模式' },
{ value: 'PROXY_CLUSTER', label: 'Proxy Cluster 模式' },
{ value: 'DIRECT', label: 'Direct 模式' },
@@ -613,11 +647,36 @@ const InstancePage = () => {
setRegions([]);
setCloudInstances([]);
}}
- onOk={() => void handleCreate()}
- confirmLoading={submitting}
- okText="连接"
- cancelText="取消"
width={520}
+ footer={
+ <Flex justify="flex-end" gap={8}>
+ {vendor === 'ALIYUN' && (
+ <Tooltip title="遍历该凭据下全部地域,将所有云上实例导入(幂等,已存在的自动跳过),备注自动取自云上实例">
+ <Button
+ loading={importing}
+ disabled={!addCredentialId}
+ onClick={() => void handleImportAll()}
+ >
+ 一键导入
+ </Button>
+ </Tooltip>
+ )}
+ <Button
+ onClick={() => {
+ setAddModalOpen(false);
+ addForm.resetFields();
+ setVendor(DEFAULT_VENDOR);
+ setRegions([]);
+ setCloudInstances([]);
+ }}
+ >
+ 取消
+ </Button>
+ <Button type="primary" loading={submitting} onClick={() => void
handleCreate()}>
+ 连接
+ </Button>
+ </Flex>
+ }
>
<Tabs
type="card"
@@ -638,92 +697,94 @@ const InstancePage = () => {
{VENDOR_OPTIONS.find((option) => option.key === vendor)?.description}
</Text>
{cloudVendor ? (
- <Form form={addForm} layout="vertical">
- <Form.Item
- label="云凭据"
- name="credentialId"
- rules={[{ required: true, message: '请选择云凭据' }]}
- extra={
- <span>
- 凭据为{vendor === 'ALIYUN' ? '阿里云' : '腾讯云'}账号的 AK/SK,
- <Link to="/settings?tab=credential">前往「设置 - 云凭据管理」添加</Link>
- </span>
- }
- >
- <Select
- placeholder="选择已录入的 AK/SK 凭据"
- loading={credentialsLoading}
- onChange={handleCredentialChange}
- notFoundContent={
- credentialsLoading ? (
- '加载中…'
- ) : (
- <span>
- 暂无{vendor === 'ALIYUN' ? '阿里云' : '腾讯云'}凭据,
- <Link to="/settings?tab=credential">去设置中添加</Link>
- </span>
- )
+ <>
+ <Form form={addForm} layout="vertical">
+ <Form.Item
+ label="云凭据"
+ name="credentialId"
+ rules={[{ required: true, message: '请选择云凭据' }]}
+ extra={
+ <span>
+ 凭据为{vendor === 'ALIYUN' ? '阿里云' : '腾讯云'}账号的 AK/SK,
+ <Link to="/settings?tab=credential">前往「设置 - 云凭据管理」添加</Link>
+ </span>
}
- options={credentials.map((item) => ({
- value: item.id,
- label: `${item.name}(${item.accessKey})`,
- }))}
- />
- </Form.Item>
- <Form.Item
- label="地域"
- name="regionId"
- rules={[{ required: true, message: '请选择地域' }]}
- >
- <Select
- placeholder={addCredentialId ? '选择地域' : '请先选择云凭据'}
- disabled={!addCredentialId}
- loading={regionsLoading}
- onChange={handleRegionChange}
- options={regions.map((region) => ({
- value: region.regionId,
- label: `${region.regionName}(${region.regionId})`,
- }))}
- />
- </Form.Item>
- <Form.Item
- label="云上实例"
- name="cloudInstanceId"
- rules={[{ required: true, message: '请选择云上实例' }]}
- extra="商业版实例来自云端目录,无法手工创建"
- >
- <Select
- showSearch
- optionFilterProp="label"
- placeholder={addRegionId ? '选择云上实例' : '请先选择地域'}
- disabled={!addRegionId}
- loading={cloudInstancesLoading}
- options={cloudInstances.map((item) => ({
- value: item.instanceId,
- label: `${item.instanceName ||
item.instanceId}(${item.instanceId})`,
- }))}
- onChange={(value) => {
- const selected = cloudInstances.find((item) =>
item.instanceId === value);
- if (selected) {
- addForm.setFieldsValue({ name: selected.instanceId });
+ >
+ <Select
+ placeholder="选择已录入的 AK/SK 凭据"
+ loading={credentialsLoading}
+ onChange={handleCredentialChange}
+ notFoundContent={
+ credentialsLoading ? (
+ '加载中…'
+ ) : (
+ <span>
+ 暂无{vendor === 'ALIYUN' ? '阿里云' : '腾讯云'}凭据,
+ <Link to="/settings?tab=credential">去设置中添加</Link>
+ </span>
+ )
}
- }}
- />
- </Form.Item>
- <Form.Item
- label="实例 ID"
- name="name"
- rules={[
- { required: true, message: '请输入实例 ID' },
- { max: 64, message: '实例 ID 不能超过 64 个字符' },
- ]}
- >
- <Input placeholder="默认取云上实例 ID" />
- </Form.Item>
- <Form.Item label="备注" name="remark">
- <Input.TextArea rows={2} placeholder="可选,描述实例用途" />
- </Form.Item>
- </Form>
+ options={credentials.map((item) => ({
+ value: item.id,
+ label: `${item.name}(${item.accessKey})`,
+ }))}
+ />
+ </Form.Item>
+ <Form.Item
+ label="地域"
+ name="regionId"
+ rules={[{ required: true, message: '请选择地域' }]}
+ >
+ <Select
+ placeholder={addCredentialId ? '选择地域' : '请先选择云凭据'}
+ disabled={!addCredentialId}
+ loading={regionsLoading}
+ onChange={handleRegionChange}
+ options={regions.map((region) => ({
+ value: region.regionId,
+ label: `${region.regionName}(${region.regionId})`,
+ }))}
+ />
+ </Form.Item>
+ <Form.Item
+ label="云上实例"
+ name="cloudInstanceId"
+ rules={[{ required: true, message: '请选择云上实例' }]}
+ extra="商业版实例来自云端目录,无法手工创建"
+ >
+ <Select
+ showSearch
+ optionFilterProp="label"
+ placeholder={addRegionId ? '选择云上实例' : '请先选择地域'}
+ disabled={!addRegionId}
+ loading={cloudInstancesLoading}
+ options={cloudInstances.map((item) => ({
+ value: item.instanceId,
+ label: `${item.instanceName ||
item.instanceId}(${item.instanceId})`,
+ }))}
+ onChange={(value) => {
+ const selected = cloudInstances.find((item) =>
item.instanceId === value);
+ if (selected) {
+ addForm.setFieldsValue({ name: selected.instanceId });
+ }
+ }}
+ />
+ </Form.Item>
+ <Form.Item
+ label="实例 ID"
+ name="name"
+ rules={[
+ { required: true, message: '请输入实例 ID' },
+ { max: 64, message: '实例 ID 不能超过 64 个字符' },
+ ]}
+ >
+ <Input placeholder="默认取云上实例 ID" />
+ </Form.Item>
+ <Form.Item label="备注" name="remark">
+ <Input.TextArea rows={2} placeholder="可选,描述实例用途" />
+ </Form.Item>
+ </Form>
+ </>
) : (
<Form form={addForm} layout="vertical">
<Form.Item
@@ -817,12 +878,15 @@ const InstancePage = () => {
rules={[{ required: true, message: '请选择接入方式' }]}
>
<Select
- options={[
- { value: 'PROXY', label: 'Proxy 模式(部署形态未标明)' },
- { value: 'PROXY_LOCAL', label: 'Proxy Local 模式' },
- { value: 'PROXY_CLUSTER', label: 'Proxy Cluster 模式' },
- { value: 'DIRECT', label: 'Direct 模式' },
- ]}
+ options={
+ editingInstance?.vendor && editingInstance.vendor !== 'APACHE'
+ ? [{ value: 'CLOUD', label: '云服务' }]
+ : [
+ { value: 'PROXY_LOCAL', label: 'Proxy Local 模式' },
+ { value: 'PROXY_CLUSTER', label: 'Proxy Cluster 模式' },
+ { value: 'DIRECT', label: 'Direct 模式' },
+ ]
+ }
/>
</Form.Item>
<Form.Item
@@ -839,8 +903,8 @@ const InstancePage = () => {
extra={
editInstanceType === 'DIRECT'
? 'Direct 模式请填写 NameServer SLB 地址(K8s 场景下一般为 NameServer
Service 地址,如 namesrv.mq.svc:9876)'
- : editInstanceType === 'PROXY'
- ? 'Proxy 模式请填写 Proxy SLB 内网地址(如 proxy.mq.svc:8080)'
+ : editInstanceType === 'CLOUD'
+ ? '云服务实例接入地址由云厂商目录解析,不支持手动修改'
: '请先选择接入方式'
}
>
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index 49ecfc123..da0cfb4ac 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -82,15 +82,15 @@ import { parseMessageProperties } from
'../../utils/messageProperties';
const { Text } = Typography;
const INSTANCE_ACCESS_LABEL: Record<Instance['type'], string> = {
- PROXY: 'Proxy 模式(部署形态未标明)',
+ CLOUD: '云服务',
PROXY_LOCAL: 'Proxy Local 模式',
PROXY_CLUSTER: 'Proxy Cluster 模式',
DIRECT: 'Direct 模式',
};
const INSTANCE_ACCESS_DESCRIPTION: Record<Instance['type'], string> = {
- PROXY:
- '接入点为 Proxy 地址,但该存量实例未标明 Local/Cluster 部署形态。若客户端环境无法解析该地址,请自行配置 DNS
解析或在客户端 hosts 中映射。',
+ CLOUD:
+ '接入点为云厂商托管实例的接入地址,由云实例目录解析得出。若客户端环境无法解析该地址,请自行配置 DNS 解析或在客户端 hosts 中映射。',
PROXY_LOCAL:
'接入点为与 Broker 同进程部署的 Proxy 地址。若客户端环境无法解析该地址,请自行配置 DNS 解析或在客户端 hosts 中映射。',
PROXY_CLUSTER:
diff --git a/web/src/services/instanceService.test.ts
b/web/src/services/instanceService.test.ts
index 49798c903..12e7d2b4a 100644
--- a/web/src/services/instanceService.test.ts
+++ b/web/src/services/instanceService.test.ts
@@ -56,21 +56,18 @@ describe('instanceService mock instances', () => {
const combined = await listInstances({ type: 'DIRECT', search:
'instance-direct-2' });
expect(combined.map((instance) => instance.id)).toEqual([2]);
- const allProxy = await listInstances({ type: 'PROXY' });
- expect(allProxy.map((instance) => instance.type)).toEqual([
- 'PROXY_CLUSTER',
- 'PROXY_CLUSTER',
- 'PROXY_LOCAL',
- ]);
+ const clusterOnly = await listInstances({ type: 'PROXY_CLUSTER' });
+ expect(clusterOnly.map((instance) => instance.id)).toEqual([3, 4]);
await expect(listInstances({ type: 'PROXY_LOCAL' })).resolves.toEqual([
expect.objectContaining({ type: 'PROXY_LOCAL' }),
]);
+ await expect(listInstances({ type: 'CLOUD' })).resolves.toEqual([]);
});
it('does not expose created or updated store records by reference', async ()
=> {
const created = await createInstance({
name: 'rocketmq-copy-test',
- type: 'PROXY',
+ type: 'PROXY_CLUSTER',
endpoint: 'proxy-copy-test:8080',
remark: 'created',
});
diff --git a/web/src/services/instanceService.ts
b/web/src/services/instanceService.ts
index bdfff2349..d50676b21 100644
--- a/web/src/services/instanceService.ts
+++ b/web/src/services/instanceService.ts
@@ -4,6 +4,7 @@ import type {
Instance,
CreateInstanceRequest,
InstanceQuery,
+ InstanceVendor,
UpdateInstanceRequest,
InstanceCapabilities,
} from '../api/instance';
@@ -17,7 +18,7 @@ function copyInstance(instance: Instance): Instance {
function matchesType(instance: Instance, type?: Instance['type']) {
if (!type) return true;
- return type === 'PROXY' ? instance.type !== 'DIRECT' : instance.type ===
type;
+ return instance.type === type;
}
const APACHE_CAPABILITIES: InstanceCapabilities['capabilities'] = [
@@ -76,11 +77,7 @@ export async function createInstance(data:
CreateInstanceRequest): Promise<Insta
id: Date.now(),
...data,
name: data.name || '',
- type: cloudManaged
- ? 'PROXY'
- : data.type === 'PROXY'
- ? 'PROXY_CLUSTER'
- : data.type || 'PROXY_CLUSTER',
+ type: cloudManaged ? 'CLOUD' : data.type || 'PROXY_CLUSTER',
endpoint: data.endpoint || '',
vendor: data.vendor || 'APACHE',
remark: data.remark || '',
@@ -95,6 +92,16 @@ export async function createInstance(data:
CreateInstanceRequest): Promise<Insta
return instanceApi.createInstance(data);
}
+export async function importCloudInstances(data: {
+ vendor: Exclude<InstanceVendor, 'APACHE'>;
+ credentialId: number;
+}): Promise<instanceApi.CloudImportResult> {
+ if (isMockMode()) {
+ return { discovered: 0, imported: 0, skipped: 0, failed: [] };
+ }
+ return instanceApi.importCloudInstances(data);
+}
+
export async function updateInstance(data: UpdateInstanceRequest):
Promise<Instance> {
if (isMockMode()) {
const { instanceId, ...changes } = data;