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 37bc9c81 feat: support server-side instance filtering (#743)
37bc9c81 is described below

commit 37bc9c8196517daaa9c46ad2096df0474cda0b95
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:17:41 2026 +0800

    feat: support server-side instance filtering (#743)
---
 .../instance/InMemoryInstanceRepository.java       |  21 ++-
 .../instance/InMemoryInstanceRepositoryTest.java   |  44 +++++
 web/src/api/instance.test.ts                       |  20 +++
 web/src/api/instance.ts                            |  14 +-
 .../pages/instance/__tests__/InstancePage.test.tsx | 188 +++++++++++++++++++++
 web/src/pages/instance/index.tsx                   |  78 +++++----
 web/src/services/instanceService.test.ts           |  11 ++
 web/src/services/instanceService.ts                |  25 ++-
 8 files changed, 358 insertions(+), 43 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepository.java
index a7c46766..ec38ba59 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepository.java
@@ -23,6 +23,7 @@ import org.springframework.stereotype.Component;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
@@ -80,23 +81,31 @@ public class InMemoryInstanceRepository implements 
InstanceRepository {
 
     @Override
     public List<InstanceVO> search(String keyword) {
-        String lower = keyword.toLowerCase();
+        String lower = keyword.toLowerCase(Locale.ROOT);
         return store.values().stream()
-                .filter(i -> i.getName().toLowerCase().contains(lower)
-                        || i.getRemark() != null && 
i.getRemark().toLowerCase().contains(lower))
+                .filter(instance -> matchesSearch(instance, lower))
                 .collect(Collectors.toList());
     }
 
     @Override
     public List<InstanceVO> findByTypeAndSearch(InstanceType type, String 
keyword) {
-        String lower = keyword.toLowerCase();
+        String lower = keyword.toLowerCase(Locale.ROOT);
         return store.values().stream()
                 .filter(i -> i.getType() == type)
-                .filter(i -> i.getName().toLowerCase().contains(lower)
-                        || i.getRemark() != null && 
i.getRemark().toLowerCase().contains(lower))
+                .filter(instance -> matchesSearch(instance, lower))
                 .collect(Collectors.toList());
     }
 
+    private boolean matchesSearch(InstanceVO instance, String lowerKeyword) {
+        return containsIgnoreCase(instance.getName(), lowerKeyword)
+                || containsIgnoreCase(instance.getEndpoint(), lowerKeyword)
+                || containsIgnoreCase(instance.getRemark(), lowerKeyword);
+    }
+
+    private boolean containsIgnoreCase(String value, String lowerKeyword) {
+        return value != null && 
value.toLowerCase(Locale.ROOT).contains(lowerKeyword);
+    }
+
     @Override
     public Optional<InstanceVO> findById(String id) {
         return Optional.ofNullable(store.get(id));
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepositoryTest.java
new file mode 100644
index 00000000..06a7bddb
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/InMemoryInstanceRepositoryTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class InMemoryInstanceRepositoryTest {
+
+    private final InMemoryInstanceRepository repository = new 
InMemoryInstanceRepository();
+
+    @Test
+    void searchShouldMatchInstanceEndpoints() {
+        assertThat(repository.search("10.0.2.100:8080"))
+                .extracting(InstanceVO::getId)
+                .containsExactly("inst-2");
+    }
+
+    @Test
+    void typeAndSearchShouldMatchEndpointsWithinTheSelectedType() {
+        assertThat(repository.findByTypeAndSearch(InstanceType.DIRECT, 
"10.0.3.100"))
+                .extracting(InstanceVO::getId)
+                .containsExactly("inst-3");
+
+        assertThat(repository.findByTypeAndSearch(InstanceType.PROXY, 
"10.0.3.100"))
+                .isEmpty();
+    }
+}
diff --git a/web/src/api/instance.test.ts b/web/src/api/instance.test.ts
index 2bfb0f98..d423e867 100644
--- a/web/src/api/instance.test.ts
+++ b/web/src/api/instance.test.ts
@@ -56,6 +56,26 @@ describe('instance API', () => {
     await expect(updateInstance({ id: instance.id, remark: 'updated' 
})).resolves.toEqual(instance);
   });
 
+  it('sends normalized instance filters', async () => {
+    mock.onGet('/instances').reply((config) => {
+      expect(config.params).toEqual({ type: 'PROXY', search: 'proxy:8080' });
+      return [200, { code: 200, data: [instance] }];
+    });
+
+    await expect(listInstances({ type: 'PROXY', search: '  proxy:8080  ' 
})).resolves.toEqual([
+      instance,
+    ]);
+  });
+
+  it('omits blank instance filters', async () => {
+    mock.onGet('/instances').reply((config) => {
+      expect(config.params).toEqual({});
+      return [200, { code: 200, data: [instance] }];
+    });
+
+    await expect(listInstances({ search: '   ' 
})).resolves.toEqual([instance]);
+  });
+
   it('sends the instance id when deleting', async () => {
     mock.onPost('/instances/delete').reply((config) => {
       expect(JSON.parse(config.data)).toEqual({ id: instance.id });
diff --git a/web/src/api/instance.ts b/web/src/api/instance.ts
index 30dfe81f..98b0609d 100644
--- a/web/src/api/instance.ts
+++ b/web/src/api/instance.ts
@@ -45,9 +45,19 @@ export interface UpdateInstanceRequest {
   remark?: string;
 }
 
+export interface InstanceQuery {
+  type?: Instance['type'];
+  search?: string;
+}
+
 // ─── Instance CRUD ──────────────────────────────────────────────
-export async function listInstances() {
-  const res = await client.get<{ data: Instance[] }>('/instances');
+export async function listInstances(query: InstanceQuery = {}) {
+  const search = query.search?.trim();
+  const params = {
+    ...(query.type ? { type: query.type } : {}),
+    ...(search ? { search } : {}),
+  };
+  const res = await client.get<{ data: Instance[] }>('/instances', { params });
   return res.data.data;
 }
 
diff --git a/web/src/pages/instance/__tests__/InstancePage.test.tsx 
b/web/src/pages/instance/__tests__/InstancePage.test.tsx
new file mode 100644
index 00000000..269679e9
--- /dev/null
+++ b/web/src/pages/instance/__tests__/InstancePage.test.tsx
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { App } from 'antd';
+import { act, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Instance } from '../../../api/instance';
+import { LangProvider } from '../../../i18n/LangContext';
+import * as instanceService from '../../../services/instanceService';
+import InstancePage from '../index';
+
+vi.mock('../../../services/instanceService', () => ({
+  createInstance: vi.fn(),
+  deleteInstance: vi.fn(),
+  listInstances: vi.fn(),
+  updateInstance: vi.fn(),
+}));
+
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: vi.fn().mockImplementation((query: string) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: vi.fn(),
+      removeListener: vi.fn(),
+      addEventListener: vi.fn(),
+      removeEventListener: vi.fn(),
+      dispatchEvent: vi.fn(),
+    })),
+  });
+});
+
+const instance = (id: string, name: string, type: Instance['type'] = 'PROXY'): 
Instance => ({
+  id,
+  name,
+  remark: '',
+  type,
+  endpoint: `${name}:8080`,
+  topicCount: 1,
+  consumerGroupCount: 1,
+  createdAt: '2026-01-01T00:00:00Z',
+  updatedAt: '2026-01-01T00:00:00Z',
+});
+
+const renderPage = () =>
+  render(
+    <App>
+      <LangProvider>
+        <InstancePage />
+      </LangProvider>
+    </App>,
+  );
+
+describe('InstancePage', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(instanceService.listInstances).mockResolvedValue([
+      instance('proxy-1', 'production-proxy'),
+      instance('direct-1', 'development-direct', 'DIRECT'),
+    ]);
+  });
+
+  it('loads server-filtered results when the search or type changes', async () 
=> {
+    const user = userEvent.setup();
+    renderPage();
+
+    expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+    expect(instanceService.listInstances).toHaveBeenCalledWith({});
+
+    fireEvent.change(screen.getByPlaceholderText('搜索实例名称或地址'), {
+      target: { value: 'proxy-hz' },
+    });
+    await waitFor(
+      () => expect(instanceService.listInstances).toHaveBeenCalledWith({ 
search: 'proxy-hz' }),
+      { timeout: 1000 },
+    );
+
+    fireEvent.change(screen.getByPlaceholderText('搜索实例名称或地址'), {
+      target: { value: '' },
+    });
+    await waitFor(() => 
expect(instanceService.listInstances).toHaveBeenLastCalledWith({}), {
+      timeout: 1000,
+    });
+
+    const typeSelect = screen.getByRole('combobox');
+    fireEvent.mouseDown(typeSelect.parentElement!);
+    await user.click(
+      await screen.findByText('Direct 模式', { selector: 
'.ant-select-item-option-content' }),
+    );
+
+    await waitFor(() =>
+      expect(instanceService.listInstances).toHaveBeenLastCalledWith({ type: 
'DIRECT' }),
+    );
+  });
+
+  it('ignores an older search response that finishes after the latest 
request', async () => {
+    let resolveOldSearch!: (instances: Instance[]) => void;
+    let resolveLatestSearch!: (instances: Instance[]) => void;
+    const oldSearch = new Promise<Instance[]>((resolve) => {
+      resolveOldSearch = resolve;
+    });
+    const latestSearch = new Promise<Instance[]>((resolve) => {
+      resolveLatestSearch = resolve;
+    });
+    vi.mocked(instanceService.listInstances)
+      .mockResolvedValueOnce([instance('initial', 'initial-instance')])
+      .mockReturnValueOnce(oldSearch)
+      .mockReturnValueOnce(latestSearch);
+    renderPage();
+
+    expect(await screen.findByText('initial-instance')).toBeInTheDocument();
+    const searchInput = screen.getByPlaceholderText('搜索实例名称或地址');
+    fireEvent.change(searchInput, { target: { value: 'old' } });
+    await waitFor(
+      () => expect(instanceService.listInstances).toHaveBeenCalledWith({ 
search: 'old' }),
+      {
+        timeout: 1000,
+      },
+    );
+
+    fireEvent.change(searchInput, { target: { value: 'latest' } });
+    await waitFor(
+      () => expect(instanceService.listInstances).toHaveBeenCalledWith({ 
search: 'latest' }),
+      { timeout: 1000 },
+    );
+
+    await act(async () => resolveLatestSearch([instance('latest', 
'latest-instance')]));
+    expect(await screen.findByText('latest-instance')).toBeInTheDocument();
+
+    await act(async () => resolveOldSearch([instance('old', 'old-instance')]));
+    expect(screen.queryByText('old-instance')).not.toBeInTheDocument();
+    expect(screen.getByText('latest-instance')).toBeInTheDocument();
+  });
+
+  it('reloads the current filters after creating an instance', async () => {
+    const user = userEvent.setup();
+    
vi.mocked(instanceService.createInstance).mockResolvedValue(instance('created', 
'new-proxy'));
+    renderPage();
+
+    expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+    const typeSelect = screen.getByRole('combobox');
+    fireEvent.mouseDown(typeSelect.parentElement!);
+    await user.click(
+      await screen.findByText('Direct 模式', { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      expect(instanceService.listInstances).toHaveBeenLastCalledWith({ type: 
'DIRECT' }),
+    );
+
+    await user.click(screen.getByRole('button', { name: /添加实例/ }));
+    const dialog = await screen.findByRole('dialog');
+    await user.type(within(dialog).getByLabelText('实例名称'), 'new-proxy');
+    const createTypeSelect = within(dialog).getByRole('combobox');
+    fireEvent.mouseDown(createTypeSelect.parentElement!);
+    const proxyOptions = await screen.findAllByText('Proxy 模式', {
+      selector: '.ant-select-item-option-content',
+    });
+    await user.click(proxyOptions[proxyOptions.length - 1]);
+    await user.type(within(dialog).getByLabelText('接入地址'), 'proxy-new:8080');
+    await user.click(within(dialog).getByRole('button', { name: /连\s*接/ }));
+
+    await waitFor(() =>
+      expect(instanceService.createInstance).toHaveBeenCalledWith({
+        name: 'new-proxy',
+        type: 'PROXY',
+        endpoint: 'proxy-new:8080',
+      }),
+    );
+    expect(instanceService.listInstances).toHaveBeenLastCalledWith({ type: 
'DIRECT' });
+  });
+});
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 3eb3d0ed..225b806d 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import {
   Table,
   Card,
@@ -34,7 +34,7 @@ import { useLang } from '../../i18n/LangContext';
 import { Plus, MagnifyingGlass } from '@phosphor-icons/react';
 import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
-import type { Instance } from '../../api/instance';
+import type { Instance, InstanceQuery } from '../../api/instance';
 import {
   createInstance,
   deleteInstance,
@@ -50,6 +50,8 @@ const typeLabel: Record<string, { text: string; color: string 
}> = {
   DIRECT: { text: 'Direct 模式', color: 'orange' },
 };
 
+type InstanceTypeFilter = 'ALL' | Instance['type'];
+
 /* ═══════════════════════════════════════════
    InstancePage
    ═══════════════════════════════════════════ */
@@ -58,38 +60,60 @@ const InstancePage = () => {
   const [instances, setInstances] = useState<Instance[]>([]);
   const [loading, setLoading] = useState(true);
   const [search, setSearch] = useState('');
-  const [typeFilter, setTypeFilter] = useState('ALL');
+  const [debouncedSearch, setDebouncedSearch] = useState('');
+  const [typeFilter, setTypeFilter] = useState<InstanceTypeFilter>('ALL');
   const [addModalOpen, setAddModalOpen] = useState(false);
   const [addForm] = Form.useForm();
   const [editModalOpen, setEditModalOpen] = useState(false);
   const [editingInstance, setEditingInstance] = useState<Instance | 
null>(null);
   const [editForm] = Form.useForm();
   const [submitting, setSubmitting] = useState(false);
+  const requestIdRef = useRef(0);
+
+  useEffect(() => {
+    const timer = window.setTimeout(() => setDebouncedSearch(search.trim()), 
300);
+    return () => window.clearTimeout(timer);
+  }, [search]);
+
+  const loadInstances = useCallback(async () => {
+    const requestId = ++requestIdRef.current;
+    const query: InstanceQuery = {
+      ...(typeFilter === 'ALL' ? {} : { type: typeFilter }),
+      ...(debouncedSearch ? { search: debouncedSearch } : {}),
+    };
+
+    setLoading(true);
+    try {
+      const nextInstances = await listInstances(query);
+      if (requestId === requestIdRef.current) {
+        setInstances(nextInstances);
+      }
+    } catch {
+      if (requestId === requestIdRef.current) {
+        message.error('实例列表加载失败,请稍后重试');
+      }
+    } finally {
+      if (requestId === requestIdRef.current) {
+        setLoading(false);
+      }
+    }
+  }, [debouncedSearch, typeFilter]);
 
   useEffect(() => {
-    let cancelled = false;
-    void listInstances()
-      .then((nextInstances) => {
-        if (!cancelled) setInstances(nextInstances);
-      })
-      .catch(() => {
-        if (!cancelled) message.error('实例列表加载失败,请稍后重试');
-      })
-      .finally(() => {
-        if (!cancelled) setLoading(false);
-      });
+    const timer = window.setTimeout(() => void loadInstances(), 0);
 
     return () => {
-      cancelled = true;
+      window.clearTimeout(timer);
+      requestIdRef.current += 1;
     };
-  }, []);
+  }, [loadInstances]);
 
   const handleCreate = async () => {
     try {
       const values = await addForm.validateFields();
       setSubmitting(true);
       const created = await createInstance(values);
-      setInstances((previous) => [...previous, created]);
+      await loadInstances();
       message.success(`实例「${created.name}」添加成功`);
       setAddModalOpen(false);
       addForm.resetFields();
@@ -106,9 +130,7 @@ const InstancePage = () => {
       const values = await editForm.validateFields();
       setSubmitting(true);
       const updated = await updateInstance({ id: editingInstance.id, remark: 
values.remark || '' });
-      setInstances((previous) =>
-        previous.map((instance) => (instance.id === updated.id ? updated : 
instance)),
-      );
+      await loadInstances();
       message.success(`实例「${updated.name}」备注已更新`);
       setEditModalOpen(false);
       editForm.resetFields();
@@ -122,20 +144,14 @@ const InstancePage = () => {
   const handleDelete = async (instance: Instance) => {
     try {
       await deleteInstance(instance.id);
-      setInstances((previous) => previous.filter((item) => item.id !== 
instance.id));
+      await loadInstances();
       message.success('已删除');
     } catch {
       message.error('删除实例失败,请稍后重试');
     }
   };
 
-  const filtered = instances
-    .filter((i) => {
-      const matchSearch = i.name.includes(search) || 
i.endpoint.includes(search);
-      const matchType = typeFilter === 'ALL' || i.type === typeFilter;
-      return matchSearch && matchType;
-    })
-    .sort((a, b) => a.name.localeCompare(b.name));
+  const sortedInstances = [...instances].sort((a, b) => 
a.name.localeCompare(b.name));
 
   const columns: ColumnsType<Instance> = [
     {
@@ -258,7 +274,7 @@ const InstancePage = () => {
       <div style={{ marginBottom: 20 }}>
         <h2 style={{ margin: 0, fontSize: 20, fontWeight: 600 
}}>{t('instance.title')}</h2>
         <span style={{ fontSize: 13, color: '#9CA3AF' }}>
-          管理 RocketMQ 集群连接,共 {instances.length} 个实例
+          管理 RocketMQ 集群连接,当前显示 {instances.length} 个实例
         </span>
       </div>
 
@@ -279,7 +295,7 @@ const InstancePage = () => {
             style={{ width: 240 }}
             allowClear
           />
-          <Select
+          <Select<InstanceTypeFilter>
             value={typeFilter}
             onChange={setTypeFilter}
             style={{ width: 140 }}
@@ -303,7 +319,7 @@ const InstancePage = () => {
       <Card bodyStyle={{ padding: 0 }}>
         <Table
           columns={columns}
-          dataSource={filtered}
+          dataSource={sortedInstances}
           loading={loading}
           rowKey="id"
           pagination={false}
diff --git a/web/src/services/instanceService.test.ts 
b/web/src/services/instanceService.test.ts
index e7f394b6..a8f93118 100644
--- a/web/src/services/instanceService.test.ts
+++ b/web/src/services/instanceService.test.ts
@@ -40,6 +40,17 @@ describe('instanceService mock instances', () => {
     expect(fresh[0]).not.toBe(instances[0]);
   });
 
+  it('filters mock instances with the same type and search semantics as the 
API', async () => {
+    const byType = await listInstances({ type: 'DIRECT' });
+    expect(byType.map((instance) => instance.id)).toEqual(['4']);
+
+    const byEndpoint = await listInstances({ search: '  PROXY-HZ  ' });
+    expect(byEndpoint.map((instance) => instance.id)).toEqual(['1']);
+
+    const combined = await listInstances({ type: 'DIRECT', search: 
'namesrv-legacy' });
+    expect(combined.map((instance) => instance.id)).toEqual(['4']);
+  });
+
   it('does not expose created or updated store records by reference', async () 
=> {
     const created = await createInstance({
       name: 'rocketmq-copy-test',
diff --git a/web/src/services/instanceService.ts 
b/web/src/services/instanceService.ts
index 5cadff45..0b6564c4 100644
--- a/web/src/services/instanceService.ts
+++ b/web/src/services/instanceService.ts
@@ -1,6 +1,11 @@
 import { USE_MOCK } from '../config';
 import * as instanceApi from '../api/instance';
-import type { Instance, CreateInstanceRequest, UpdateInstanceRequest } from 
'../api/instance';
+import type {
+  Instance,
+  CreateInstanceRequest,
+  InstanceQuery,
+  UpdateInstanceRequest,
+} from '../api/instance';
 import { mockInstances } from '../mock/instances';
 
 // Compile-time switch: mock or real API
@@ -10,9 +15,21 @@ function copyInstance(instance: Instance): Instance {
   return { ...instance };
 }
 
-export async function listInstances(): Promise<Instance[]> {
-  if (USE_MOCK) return mockInstances.map(copyInstance);
-  return instanceApi.listInstances();
+export async function listInstances(query: InstanceQuery = {}): 
Promise<Instance[]> {
+  if (USE_MOCK) {
+    const search = query.search?.trim().toLowerCase();
+    return mockInstances
+      .filter((instance) => !query.type || instance.type === query.type)
+      .filter(
+        (instance) =>
+          !search ||
+          [instance.name, instance.endpoint, instance.remark].some((value) =>
+            value.toLowerCase().includes(search),
+          ),
+      )
+      .map(copyInstance);
+  }
+  return instanceApi.listInstances(query);
 }
 
 export async function createInstance(data: CreateInstanceRequest): 
Promise<Instance> {

Reply via email to