This is an automated email from the ASF dual-hosted git repository.

LiteSun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 89fc2ed17 feat: show a translated empty state on every list page 
(#3454)
89fc2ed17 is described below

commit 89fc2ed17cf5ca21551d113ce79cd0899383cbec
Author: Yuhan <[email protected]>
AuthorDate: Mon Aug 3 09:25:46 2026 +0800

    feat: show a translated empty state on every list page (#3454)
---
 .../integration/empty-state.all-resources.spec.ts  | 18 ++++++++
 .../regression/credentials.empty-state.spec.ts     | 50 ++++++++++++++++++++++
 e2e/tests/services.empty.spec.ts                   |  3 +-
 src/components/page/ListEmptyState.tsx             | 44 +++++++++++++++++++
 src/locales/de/common.json                         |  6 ++-
 src/locales/en/common.json                         |  6 ++-
 src/locales/es/common.json                         |  6 ++-
 src/locales/tr/common.json                         |  6 ++-
 src/locales/zh/common.json                         |  6 ++-
 src/routes/consumer_groups/index.tsx               |  4 ++
 .../detail.$username/credentials/index.tsx         |  4 ++
 src/routes/consumers/index.tsx                     |  4 ++
 src/routes/global_rules/index.tsx                  |  4 ++
 src/routes/plugin_configs/index.tsx                |  4 ++
 src/routes/protos/index.tsx                        |  4 ++
 src/routes/routes/index.tsx                        |  4 ++
 src/routes/secrets/index.tsx                       |  4 ++
 src/routes/services/index.tsx                      |  9 +---
 src/routes/ssls/index.tsx                          |  4 ++
 src/routes/stream_routes/index.tsx                 |  4 ++
 src/routes/upstreams/index.tsx                     |  4 ++
 21 files changed, 179 insertions(+), 19 deletions(-)

diff --git a/e2e/tests/integration/empty-state.all-resources.spec.ts 
b/e2e/tests/integration/empty-state.all-resources.spec.ts
index 2eb0ae46e..cb0386397 100644
--- a/e2e/tests/integration/empty-state.all-resources.spec.ts
+++ b/e2e/tests/integration/empty-state.all-resources.spec.ts
@@ -31,6 +31,7 @@ import { deleteAllConsumers } from '@/apis/consumers';
 import { deleteAllRoutes } from '@/apis/routes';
 import { deleteAllServices } from '@/apis/services';
 import { deleteAllSSLs } from '@/apis/ssls';
+import { deleteAllStreamRoutes } from '@/apis/stream_routes';
 import { deleteAllUpstreams } from '@/apis/upstreams';
 
 type Resource = {
@@ -39,6 +40,12 @@ type Resource = {
   apiPath: string;
 };
 
+// `secrets` is deliberately absent: its Admin API key is compound
+// (`/secrets/{manager}/{id}`) while the list response has already split
+// `manager` out of `value.id`, so the generic `purge()` below would issue
+// `DELETE /secrets/{id}` and silently leave rows behind — the empty-state
+// assertion would then fail for the wrong reason. Covering it needs a
+// manager-aware cleanup.
 const RESOURCES: Resource[] = [
   { path: '/services', label: 'Services', apiPath: '/services' },
   { path: '/routes', label: 'Routes', apiPath: '/routes' },
@@ -57,6 +64,11 @@ const RESOURCES: Resource[] = [
     apiPath: '/plugin_configs',
   },
   { path: '/protos', label: 'Protos', apiPath: '/protos' },
+  {
+    path: '/stream_routes',
+    label: 'Stream Routes',
+    apiPath: '/stream_routes',
+  },
 ];
 
 // Inline delete-all for resources that don't have a product-side helper.
@@ -86,6 +98,7 @@ test.beforeAll(async () => {
     () => deleteAllConsumers(e2eReq),
     () => deleteAllConsumerGroups(e2eReq),
     () => deleteAllSSLs(e2eReq),
+    () => deleteAllStreamRoutes(e2eReq),
     () => purge('/global_rules'),
     () => purge('/plugin_configs'),
     () => purge('/protos')
@@ -105,6 +118,11 @@ for (const resource of RESOURCES) {
       page.getByRole('heading', { name: resource.label, exact: true })
     ).toBeVisible();
 
+    // The empty state must name the resource, not just avoid leaking keys.
+    // `resource.label` is the same plural noun the message interpolates, so
+    // this also proves the interpolation resolved.
+    await expect(page.getByText(`No ${resource.label} yet`)).toBeVisible();
+
     // Hard-fail symptoms of #3321 — raw translation key in the visible UI.
     const bodyText = await page.locator('body').innerText();
     for (const suspect of [
diff --git a/e2e/tests/regression/credentials.empty-state.spec.ts 
b/e2e/tests/regression/credentials.empty-state.spec.ts
new file mode 100644
index 000000000..d61aefdf1
--- /dev/null
+++ b/e2e/tests/regression/credentials.empty-state.spec.ts
@@ -0,0 +1,50 @@
+/**
+ * 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 { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiGoto } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+import { deleteAllConsumers, putConsumerReq } from '@/apis/consumers';
+import type { APISIXType } from '@/types/schema/apisix';
+
+// Routes and stream routes also render nested (under
+// /services/detail/$id/...), but this spec is the only coverage of the
+// credentials list specifically, so it stays separate from the all-resources
+// loop above.
+
+const USERNAME = 'reg_empty_state_consumer';
+
+// A fresh consumer has no credentials, which is exactly the state under test.
+test.beforeAll(async () => {
+  await putConsumerReq(e2eReq, {
+    username: USERNAME,
+  } as APISIXType['ConsumerPut']);
+});
+
+test.afterAll(async () => {
+  await deleteAllConsumers(e2eReq);
+});
+
+test('the credentials empty state names the resource on a nested list', async 
({
+  page,
+}) => {
+  await uiGoto(page, `/consumers/detail/${USERNAME}/credentials`);
+
+  await expect(page.getByText('No Credentials yet')).toBeVisible();
+});
diff --git a/e2e/tests/services.empty.spec.ts b/e2e/tests/services.empty.spec.ts
index a616b18d6..baeb7dc3d 100644
--- a/e2e/tests/services.empty.spec.ts
+++ b/e2e/tests/services.empty.spec.ts
@@ -39,8 +39,7 @@ test.describe('Services Empty State', () => {
             const table = page.getByRole('table');
             await expect(table).toBeVisible();
 
-            const emptyDescription = page.getByText('No services found. Click 
Add Service to create your first one.');
-            await expect(emptyDescription).toBeVisible();
+            await expect(page.getByText('No Services yet')).toBeVisible();
 
             const emptyImage = page.locator('.ant-empty-image');
             await expect(emptyImage).toBeVisible();
diff --git a/src/components/page/ListEmptyState.tsx 
b/src/components/page/ListEmptyState.tsx
new file mode 100644
index 000000000..b18765720
--- /dev/null
+++ b/src/components/page/ListEmptyState.tsx
@@ -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.
+ */
+import { Empty } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+import type { Resources } from '@/config/i18n';
+
+export type ListEmptyStateProps = {
+  /**
+   * Resource key, used to look up the already-translated plural name in
+   * `sources.*`. Typing it this way means a typo cannot compile.
+   */
+  resource: keyof Resources['en']['common']['sources'];
+};
+
+/**
+ * Shown in place of an empty list. Deliberately text-only: the toolbar's Add
+ * button sits directly above the table, so repeating it here would be a
+ * duplicate control rather than a shortcut.
+ */
+export const ListEmptyState = (props: ListEmptyStateProps) => {
+  const { resource } = props;
+  const { t } = useTranslation();
+  return (
+    <Empty
+      image={Empty.PRESENTED_IMAGE_SIMPLE}
+      description={t('empty.description', { name: t(`sources.${resource}`) })}
+    />
+  );
+};
diff --git a/src/locales/de/common.json b/src/locales/de/common.json
index 81c40c901..87cac851f 100644
--- a/src/locales/de/common.json
+++ b/src/locales/de/common.json
@@ -13,6 +13,9 @@
     "singular": "Anmeldeinformation"
   },
   "docs": "Dokumentation",
+  "empty": {
+    "description": "Noch keine {{name}}"
+  },
   "form": {
     "basic": {
       "desc": "Beschreibung",
@@ -348,8 +351,7 @@
     "singular": "Secret"
   },
   "services": {
-    "singular": "Service",
-    "empty": "Keine Services gefunden. Klicken Sie auf Service hinzufügen, um 
Ihren ersten zu erstellen."
+    "singular": "Service"
   },
   "settings": {
     "adminKey": "Admin Key",
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 8020f6da1..ae22e2c6f 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -13,6 +13,9 @@
     "singular": "Credential"
   },
   "docs": "Docs",
+  "empty": {
+    "description": "No {{name}} yet"
+  },
   "form": {
     "basic": {
       "desc": "Description",
@@ -348,8 +351,7 @@
     "singular": "Secret"
   },
   "services": {
-    "singular": "Service",
-    "empty": "No services found. Click Add Service to create your first one."
+    "singular": "Service"
   },
   "settings": {
     "adminKey": "Admin Key",
diff --git a/src/locales/es/common.json b/src/locales/es/common.json
index 693e09db0..e62229622 100644
--- a/src/locales/es/common.json
+++ b/src/locales/es/common.json
@@ -13,6 +13,9 @@
     "singular": "Credencial"
   },
   "docs": "Documentación",
+  "empty": {
+    "description": "Aún no hay {{name}}"
+  },
   "form": {
     "basic": {
       "desc": "Descripción",
@@ -348,8 +351,7 @@
     "singular": "Secreto"
   },
   "services": {
-    "singular": "Servicio",
-    "empty": "No se encontraron servicios. Haga clic en Añadir Servicio para 
crear el primero."
+    "singular": "Servicio"
   },
   "settings": {
     "adminKey": "Clave de Administrador",
diff --git a/src/locales/tr/common.json b/src/locales/tr/common.json
index e2b48a65e..91909d3b7 100644
--- a/src/locales/tr/common.json
+++ b/src/locales/tr/common.json
@@ -13,6 +13,9 @@
     "singular": "Credential"
   },
   "docs": "Dokümanlar",
+  "empty": {
+    "description": "Henüz {{name}} yok"
+  },
   "form": {
     "basic": {
       "desc": "Açıklama",
@@ -348,8 +351,7 @@
     "singular": "Secret"
   },
   "services": {
-    "singular": "Service",
-    "empty": "Service bulunamadı. İlk Service'inizi oluşturmak için Service 
Ekle'ye tıklayın."
+    "singular": "Service"
   },
   "settings": {
     "adminKey": "Admin Key",
diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json
index 7223c63c5..6a6ca3fc4 100644
--- a/src/locales/zh/common.json
+++ b/src/locales/zh/common.json
@@ -13,6 +13,9 @@
     "singular": "凭证"
   },
   "docs": "文档",
+  "empty": {
+    "description": "还没有{{name}}"
+  },
   "form": {
     "basic": {
       "desc": "描述",
@@ -348,8 +351,7 @@
     "singular": "Secret"
   },
   "services": {
-    "singular": "服务",
-    "empty": "未找到服务。创建您的第一个服务。"
+    "singular": "服务"
   },
   "settings": {
     "adminKey": "管理员密钥",
diff --git a/src/routes/consumer_groups/index.tsx 
b/src/routes/consumer_groups/index.tsx
index a5cd679ee..e0a9522ab 100644
--- a/src/routes/consumer_groups/index.tsx
+++ b/src/routes/consumer_groups/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getConsumerGroupListQueryOptions, useConsumerGroupList } from 
'@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -95,6 +96,9 @@ function ConsumerGroupsList() {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="consumerGroups" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="id"
diff --git a/src/routes/consumers/detail.$username/credentials/index.tsx 
b/src/routes/consumers/detail.$username/credentials/index.tsx
index d433ba2b5..46d01d73f 100644
--- a/src/routes/consumers/detail.$username/credentials/index.tsx
+++ b/src/routes/consumers/detail.$username/credentials/index.tsx
@@ -25,6 +25,7 @@ import {
   useCredentialsList,
 } from '@/apis/hooks';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -95,6 +96,9 @@ function CredentialsList() {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="credentials" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="id"
diff --git a/src/routes/consumers/index.tsx b/src/routes/consumers/index.tsx
index 35f966783..a316ca56a 100644
--- a/src/routes/consumers/index.tsx
+++ b/src/routes/consumers/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getConsumerListQueryOptions, useConsumerList } from '@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -87,6 +88,9 @@ function ConsumersList() {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="consumers" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="username"
diff --git a/src/routes/global_rules/index.tsx 
b/src/routes/global_rules/index.tsx
index bc0325e19..f6b236f32 100644
--- a/src/routes/global_rules/index.tsx
+++ b/src/routes/global_rules/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getGlobalRuleListQueryOptions, useGlobalRuleList } from 
'@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -85,6 +86,9 @@ function GlobalRulesList() {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="globalRules" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="id"
diff --git a/src/routes/plugin_configs/index.tsx 
b/src/routes/plugin_configs/index.tsx
index 1716b179b..690536dcc 100644
--- a/src/routes/plugin_configs/index.tsx
+++ b/src/routes/plugin_configs/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getPluginConfigListQueryOptions, usePluginConfigList } from 
'@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -95,6 +96,9 @@ function PluginConfigsList() {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="pluginConfigs" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="id"
diff --git a/src/routes/protos/index.tsx b/src/routes/protos/index.tsx
index c1b1075fa..3249614bd 100644
--- a/src/routes/protos/index.tsx
+++ b/src/routes/protos/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getProtoListQueryOptions, useProtoList } from '@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -75,6 +76,9 @@ function RouteComponent() {
       <PageHeader title={t('sources.protos')} />
       <AntdConfigProvider>
         <ProTable
+          locale={{
+            emptyText: <ListEmptyState resource="protos" />,
+          }}
           columns={columns}
           dataSource={data?.list || []}
           rowKey="id"
diff --git a/src/routes/routes/index.tsx b/src/routes/routes/index.tsx
index d7af823da..773c83864 100644
--- a/src/routes/routes/index.tsx
+++ b/src/routes/routes/index.tsx
@@ -24,6 +24,7 @@ import { getRouteListQueryOptions, useRouteList } from 
'@/apis/hooks';
 import type { WithServiceIdFilter } from '@/apis/routes';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -98,6 +99,9 @@ export const RouteList = (props: RouteListProps) => {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="routes" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="id"
diff --git a/src/routes/secrets/index.tsx b/src/routes/secrets/index.tsx
index 769426a55..8d61834bb 100644
--- a/src/routes/secrets/index.tsx
+++ b/src/routes/secrets/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getSecretListQueryOptions, useSecretList } from '@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -83,6 +84,9 @@ function SecretList() {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="secrets" />,
+        }}
         columns={columns}
         dataSource={data?.list || []}
         rowKey="id"
diff --git a/src/routes/services/index.tsx b/src/routes/services/index.tsx
index d507ae626..e6e5d7d6b 100644
--- a/src/routes/services/index.tsx
+++ b/src/routes/services/index.tsx
@@ -17,13 +17,13 @@
 import type { ProColumns } from '@ant-design/pro-components';
 import { ProTable } from '@ant-design/pro-components';
 import { createFileRoute } from '@tanstack/react-router';
-import { Empty } from 'antd';
 import { useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { getServiceListQueryOptions, useServiceList } from '@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -95,12 +95,7 @@ const ServiceList = () => {
     <AntdConfigProvider>
       <ProTable
         locale={{
-          emptyText: (
-            <Empty
-              description={t('services.empty')}
-              image={Empty.PRESENTED_IMAGE_SIMPLE}
-            />
-          ),
+          emptyText: <ListEmptyState resource="services" />,
         }}
         columns={columns}
         dataSource={data.list}
diff --git a/src/routes/ssls/index.tsx b/src/routes/ssls/index.tsx
index 92e0f6a72..10b1f9661 100644
--- a/src/routes/ssls/index.tsx
+++ b/src/routes/ssls/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getSSLListQueryOptions, useSSLList } from '@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -95,6 +96,9 @@ function RouteComponent() {
       <PageHeader title={t('sources.ssls')} />
       <AntdConfigProvider>
         <ProTable
+          locale={{
+            emptyText: <ListEmptyState resource="ssls" />,
+          }}
           columns={columns}
           dataSource={data?.list}
           rowKey="id"
diff --git a/src/routes/stream_routes/index.tsx 
b/src/routes/stream_routes/index.tsx
index 95209ffc5..38f40f75d 100644
--- a/src/routes/stream_routes/index.tsx
+++ b/src/routes/stream_routes/index.tsx
@@ -24,6 +24,7 @@ import { getStreamRouteListQueryOptions, useStreamRouteList } 
from '@/apis/hooks
 import type { WithServiceIdFilter } from '@/apis/routes';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { StreamRoutesErrorComponent } from 
'@/components/page-slice/stream_routes/ErrorComponent';
@@ -104,6 +105,9 @@ export const StreamRouteList = (props: 
StreamRouteListProps) => {
   return (
     <AntdConfigProvider>
       <ProTable
+        locale={{
+          emptyText: <ListEmptyState resource="streamRoutes" />,
+        }}
         columns={columns}
         dataSource={data.list}
         rowKey="id"
diff --git a/src/routes/upstreams/index.tsx b/src/routes/upstreams/index.tsx
index 55a014184..5d47814c3 100644
--- a/src/routes/upstreams/index.tsx
+++ b/src/routes/upstreams/index.tsx
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { getUpstreamListQueryOptions, useUpstreamList } from '@/apis/hooks';
 import { CopyableId } from '@/components/page/CopyableId';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
+import { ListEmptyState } from '@/components/page/ListEmptyState';
 import PageHeader from '@/components/page/PageHeader';
 import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
 import { AntdConfigProvider } from '@/config/antdConfigProvider';
@@ -96,6 +97,9 @@ function RouteComponent() {
       <PageHeader title={t('sources.upstreams')} />
       <AntdConfigProvider>
         <ProTable
+          locale={{
+            emptyText: <ListEmptyState resource="upstreams" />,
+          }}
           columns={columns}
           dataSource={data?.list}
           rowKey="id"

Reply via email to