feat(connection): 优化数据源选择并支持全局搜索

- 新增全部分类、跨分类名称与类型键搜索及空结果提示
- 将卡片网格重构为响应式平面目录并完善键盘可达性
- 补齐六语言文案及语言切换、状态重置与过滤行为回归
This commit is contained in:
Syngnat
2026-07-26 23:13:51 +08:00
parent daacd95193
commit cc24d8213a
10 changed files with 462 additions and 100 deletions

View File

@@ -421,6 +421,36 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check
border-top: none !important;
}
.gn-connection-type-row {
background: transparent;
}
.gn-connection-type-row:hover {
background: rgba(16, 24, 40, 0.05);
}
.gn-connection-type-row:focus-visible {
outline: 2px solid color-mix(in srgb, var(--gn-accent, #1677ff) 68%, transparent);
outline-offset: -2px;
}
body[data-theme='dark'] .gn-connection-type-row:hover {
background: rgba(255, 255, 255, 0.06);
}
.gn-connection-type-group:hover:not([aria-pressed='true']) {
background: rgba(16, 24, 40, 0.035) !important;
}
.gn-connection-type-group:focus-visible {
outline: 2px solid color-mix(in srgb, var(--gn-accent, #1677ff) 68%, transparent);
outline-offset: -2px;
}
body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true']) {
background: rgba(255, 255, 255, 0.045) !important;
}
/* Custom Title Bar Close Button Hover */
body[data-platform='windows'] .titlebar-window-controls {
gap: 8px;

View File

@@ -4,6 +4,7 @@ import { act, create, type ReactTestRenderer } from "react-test-renderer";
import { readFileSync } from "node:fs";
import { setCurrentLanguage } from "../i18n";
import { getAllConnectionTypeCatalogItems } from "../utils/connectionTypeCatalog";
import { getCustomConnectionDriverHelp } from "../utils/driverImportGuidance";
const storeState = {
@@ -66,7 +67,38 @@ const findClickableByAnyText = (renderer: ReactTestRenderer, texts: string[]) =>
)[0];
const findClickableCard = (renderer: ReactTestRenderer, text: string) =>
renderer.root.findAll((node) => node.props?.role === "button" && textContent(node).includes(text))[0];
renderer.root.findAll(
(node) =>
(node.props?.role === "button" ||
typeof node.props?.["data-connection-type-key"] === "string") &&
textContent(node).includes(text),
)[0];
const findConnectionTypeButtons = (renderer: ReactTestRenderer) =>
renderer.root.findAll(
(node) =>
node.type === "button" &&
typeof node.props?.["data-connection-type-key"] === "string",
);
const findConnectionTypeGroup = (
renderer: ReactTestRenderer,
groupKey: string,
) =>
renderer.root.findAll(
(node) =>
node.type === "button" &&
node.props?.["data-connection-group-key"] === groupKey,
)[0];
const findInputByPlaceholder = (
renderer: ReactTestRenderer,
placeholder: string,
) =>
renderer.root.findAll(
(node) =>
node.type === "input" && node.props?.placeholder === placeholder,
)[0];
/**
* 展开「连接 URI」分组。
@@ -173,6 +205,7 @@ vi.mock("@ant-design/icons", () => {
ThunderboltOutlined: Icon,
DownOutlined: Icon,
RightOutlined: Icon,
SearchOutlined: Icon,
};
});
@@ -385,6 +418,14 @@ describe("ConnectionModal i18n", () => {
});
expect(textContent(renderer!.toJSON())).toContain("选择数据源类型");
expect(
textContent(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.all",
),
),
).toBe("全部");
await act(async () => {
storeState.setLanguagePreference("en-US");
@@ -392,9 +433,58 @@ describe("ConnectionModal i18n", () => {
expect(storeState.setLanguagePreference).toHaveBeenCalledWith("en-US");
expect(textContent(renderer!.toJSON())).toContain("Select connection type");
expect(
textContent(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.all",
),
),
).toBe("All");
expect(
textContent(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.other",
),
),
).toBe("Other");
expect(textContent(renderer!.toJSON())).not.toContain("选择数据源类型");
});
it("retranslates data source groups when resolved system language changes", async () => {
storeState.languagePreference = "system";
setCurrentLanguage("zh-CN");
const { default: ConnectionModal } = await import("./ConnectionModal");
const onClose = vi.fn();
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<ConnectionModal open onClose={onClose} />);
});
expect(
textContent(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.all",
),
),
).toBe("全部");
setCurrentLanguage("en-US");
await act(async () => {
renderer!.update(<ConnectionModal open onClose={onClose} />);
});
expect(
textContent(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.all",
),
),
).toBe("All");
});
it.each(["legacy", "v2"] as const)(
"renders localized create flow copy for %s ui",
async (uiVersion) => {
@@ -1239,6 +1329,116 @@ describe("ConnectionModal i18n", () => {
expect(pageText).not.toContain("Custom (自定义)");
});
it("searches across all data sources, keeps category state consistent, and resets on reopen", async () => {
storeState.appearance.uiVersion = "legacy";
setCurrentLanguage("en-US");
const { default: ConnectionModal } = await import("./ConnectionModal");
const onClose = vi.fn();
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<ConnectionModal open onClose={onClose} />);
});
const expectedKeys = new Set(
getAllConnectionTypeCatalogItems().map((item) => item.key),
);
expect(
findConnectionTypeButtons(renderer!).map(
(node) => node.props["data-connection-type-key"],
),
).toEqual([...expectedKeys]);
await act(async () => {
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.nosql",
).props.onClick();
});
expect(
findConnectionTypeButtons(renderer!).map(
(node) => node.props["data-connection-type-key"],
),
).toEqual(["mongodb", "redis", "elasticsearch"]);
await act(async () => {
findInputByPlaceholder(
renderer!,
"Search data source name",
).props.onChange({
target: { value: " DIROS " },
});
});
expect(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.all",
).props["aria-pressed"],
).toBe(true);
expect(
findConnectionTypeButtons(renderer!).map(
(node) => node.props["data-connection-type-key"],
),
).toEqual(["diros"]);
expect(textContent(renderer!.toJSON())).toContain("Doris");
await act(async () => {
findInputByPlaceholder(
renderer!,
"Search data source name",
).props.onChange({
target: { value: "not-a-real-data-source" },
});
});
expect(findConnectionTypeButtons(renderer!)).toHaveLength(0);
expect(textContent(renderer!.toJSON())).toContain(
"No matching data source",
);
await act(async () => {
renderer!.update(<ConnectionModal open={false} onClose={onClose} />);
});
await act(async () => {
renderer!.update(<ConnectionModal open onClose={onClose} />);
});
expect(
findInputByPlaceholder(renderer!, "Search data source name").props.value,
).toBe("");
expect(findConnectionTypeButtons(renderer!)).toHaveLength(
expectedKeys.size,
);
await act(async () => {
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.other",
).props.onClick();
});
expect(
findConnectionTypeButtons(renderer!).map(
(node) => node.props["data-connection-type-key"],
),
).toEqual(["jvm", "custom"]);
});
it("uses native buttons for category and data source keyboard interaction", async () => {
const { default: ConnectionModal } = await import("./ConnectionModal");
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
});
expect(
findConnectionTypeGroup(
renderer!,
"connection_modal.step1.group.all",
).props.type,
).toBe("button");
expect(findClickableCard(renderer!, "MySQL").type).toBe("button");
expect(findClickableCard(renderer!, "MySQL").props.type).toBe("button");
});
it("renders English custom driver DSN copy after the module was loaded in another language", async () => {
storeState.appearance.uiVersion = "legacy";
setCurrentLanguage("zh-CN");

View File

@@ -9,9 +9,6 @@ import {
Checkbox,
Select,
Alert,
Card,
Row,
Col,
Typography,
Space,
Table,
@@ -36,6 +33,7 @@ import {
ThunderboltOutlined,
DownOutlined,
RightOutlined,
SearchOutlined,
} from "@ant-design/icons";
import {
getDbIcon,
@@ -345,6 +343,7 @@ const ConnectionModal: React.FC<{
const [dbType, setDbType] = useState("mysql");
const [step, setStep] = useState(1); // 1: Select Type, 2: Configure
const [activeGroup, setActiveGroup] = useState(0); // Active category index in step 1
const [dbTypeQuery, setDbTypeQuery] = useState("");
const [activeConfigSection, setActiveConfigSection] = useState<
"basic" | "network" | "appearance"
>("basic");
@@ -1676,6 +1675,7 @@ const ConnectionModal: React.FC<{
setUseHttpTunnel(false);
setDbType("mysql");
setActiveGroup(0);
setDbTypeQuery("");
setActiveConfigSection("basic");
setActiveNetworkConfig("ssl");
setPrimaryPasswordVisible(false);
@@ -2356,17 +2356,54 @@ const ConnectionModal: React.FC<{
const driverStatusChecking =
hasCurrentDriverType && !driverStatusLoaded && step === 2;
const dbTypeGroups = useMemo(
() =>
buildConnectionTypeGroups(t).map((group) => ({
...group,
items: group.items.map((item) => ({
...item,
icon: getDbIcon(item.key, undefined, 36),
})),
})),
[],
);
// 翻译函数读取运行时语言;系统语言变化时 preference 仍可能保持 "system"
// 因此这组少量目录项必须随组件重渲染重新派生,不能只按 preference 做 memo。
const localizedDbTypeGroups = buildConnectionTypeGroups(t).map((group) => ({
...group,
items: group.items.map((item) => ({
...item,
icon: getDbIcon(item.key, undefined, 32),
})),
}));
const seenDbTypeKeys = new Set<string>();
const allDbTypeItems = localizedDbTypeGroups
.flatMap((group) => group.items)
.filter((item) => {
if (seenDbTypeKeys.has(item.key)) {
return false;
}
seenDbTypeKeys.add(item.key);
return true;
});
const dbTypeGroups = [
{
labelKey: "connection_modal.step1.group.all",
label: t("connection.modal.step1.group.all"),
items: allDbTypeItems,
},
...localizedDbTypeGroups,
];
const normalizedDbTypeQuery = dbTypeQuery.trim().toLowerCase();
const visibleDbTypeItems = normalizedDbTypeQuery
? (dbTypeGroups[0]?.items ?? []).filter(
(item) =>
item.name.toLowerCase().includes(normalizedDbTypeQuery) ||
String(item.key).toLowerCase().includes(normalizedDbTypeQuery),
)
: (dbTypeGroups[activeGroup]?.items ?? []);
const handleDbTypeQueryChange = (value: string) => {
setDbTypeQuery(value);
if (value.trim()) {
setActiveGroup(0);
}
};
const handleDbTypeGroupSelect = (index: number) => {
setActiveGroup(index);
setDbTypeQuery("");
};
const dbTypes = getAllConnectionTypeCatalogItems();
@@ -2379,20 +2416,46 @@ const ConnectionModal: React.FC<{
height: "100%",
}}
>
<div style={{ ...modalInnerSectionStyle, paddingBottom: 12 }}>
<div
<div
style={{
...modalInnerSectionStyle,
display: "flex",
alignItems: "flex-end",
justifyContent: "space-between",
gap: 24,
padding: "8px 12px 14px",
flexWrap: "wrap",
}}
>
<div style={{ minWidth: 240, flex: "1 1 360px" }}>
<div
style={{
marginBottom: 5,
color: darkMode ? "#f5f7ff" : "#162033",
fontSize: 14,
fontWeight: 700,
}}
>
{t("connection.modal.step1.sectionTitle")}
</div>
<div style={modalMutedTextStyle}>
{t("connection.modal.step1.sectionDescription")}
</div>
</div>
<Input
allowClear
aria-label={t("connection.modal.step1.search.placeholder")}
value={dbTypeQuery}
onChange={(event) => handleDbTypeQueryChange(event.target.value)}
placeholder={t("connection.modal.step1.search.placeholder")}
prefix={<SearchOutlined style={{ color: overlayTheme.mutedText }} />}
style={{
marginBottom: 12,
color: darkMode ? "#f5f7ff" : "#162033",
fontSize: 14,
fontWeight: 700,
width: "100%",
maxWidth: 320,
flex: "0 1 320px",
}}
>
{t("connection.modal.step1.sectionTitle")}
</div>
<div style={modalMutedTextStyle}>
{t("connection.modal.step1.sectionDescription")}
</div>
{...noAutoCapInputProps}
/>
</div>
{typeSelectWarning && (
<Alert
@@ -2423,42 +2486,55 @@ const ConnectionModal: React.FC<{
display: "flex",
flex: 1,
minHeight: 0,
padding: 12,
padding: "2px 12px 8px",
}}
>
{/* 左侧分类导航 */}
<div
role="navigation"
aria-label={t("connection.modal.step1.sectionTitle")}
style={{
width: 148,
width: 142,
borderRight: `1px solid ${step1SidebarDividerColor}`,
paddingRight: 10,
paddingRight: 12,
flexShrink: 0,
overflowY: "auto",
}}
>
{dbTypeGroups.map((group, idx) => (
<div
key={group.label}
onClick={() => setActiveGroup(idx)}
<button
key={group.labelKey}
type="button"
className="gn-connection-type-group"
data-connection-group-key={group.labelKey}
aria-pressed={activeGroup === idx}
onClick={() => handleDbTypeGroupSelect(idx)}
style={{
padding: "11px 12px",
width: "100%",
padding: "8px 10px",
cursor: "pointer",
borderRadius: 12,
marginBottom: 6,
border: "none",
borderRadius: 0,
borderLeft: `2px solid ${
activeGroup === idx ? step1SidebarActiveColor : "transparent"
}`,
marginBottom: 2,
background:
activeGroup === idx ? step1SidebarActiveBg : "transparent",
activeGroup === idx
? `linear-gradient(90deg, ${step1SidebarActiveBg}, transparent)`
: "transparent",
color:
activeGroup === idx ? step1SidebarActiveColor : undefined,
fontWeight: activeGroup === idx ? 700 : 500,
transition: "all 0.2s",
transition: "background 120ms ease, color 120ms ease",
fontSize: 13,
fontFamily: "var(--gn-font-sans)",
textAlign: "left",
}}
>
{group.label}
</div>
</button>
))}
</div>
{/* 右侧数据源卡片 */}
<div
style={{
flex: 1,
@@ -2468,72 +2544,98 @@ const ConnectionModal: React.FC<{
overflowX: "hidden",
}}
>
<Row gutter={[14, 14]}>
{dbTypeGroups[activeGroup]?.items.map((item) => (
<Col span={12} key={item.key}>
<Card
hoverable
onClick={() => {
void handleTypeSelect(item.key);
}}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
gap: "2px 12px",
}}
>
{visibleDbTypeItems.map((item) => (
<button
key={item.key}
type="button"
data-connection-type-key={item.key}
aria-label={item.name}
onClick={() => {
void handleTypeSelect(item.key);
}}
className="gn-connection-type-row"
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 10,
minHeight: 44,
padding: "6px 8px",
border: "none",
borderRadius: 6,
color: "inherit",
cursor: "pointer",
fontFamily: "var(--gn-font-sans)",
textAlign: "left",
transition: "background 120ms ease",
}}
>
<div
style={{
cursor: "pointer",
minHeight: 92,
borderRadius: 16,
border: darkMode
? "1px solid rgba(255,255,255,0.08)"
: "1px solid rgba(16,24,40,0.08)",
background: darkMode
? "rgba(255,255,255,0.03)"
: "rgba(255,255,255,0.80)",
}}
styles={{
body: {
padding: 14,
display: "flex",
alignItems: "flex-start",
gap: 12,
height: "100%",
},
width: 32,
height: 32,
display: "grid",
placeItems: "center",
flexShrink: 0,
}}
>
<div
{item.icon}
</div>
<div
style={{
minWidth: 0,
flex: 1,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<Text
strong
style={{
width: 44,
height: 44,
borderRadius: 14,
display: "grid",
placeItems: "center",
flexShrink: 0,
background: darkMode
? "rgba(255,255,255,0.05)"
: "rgba(22,119,255,0.08)",
fontSize: 13.5,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
display: "block",
lineHeight: 1.2,
}}
>
{item.icon}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<Text
strong
style={{
fontSize: 14,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
display: "block",
}}
>
{item.name}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{getConnectionTypeHint(item.key, t)}
</Text>
</div>
</Card>
</Col>
{item.name}
</Text>
<Text
type="secondary"
style={{
fontSize: 11.5,
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
lineHeight: 1.2,
}}
>
{getConnectionTypeHint(item.key, t)}
</Text>
</div>
</button>
))}
</Row>
</div>
{normalizedDbTypeQuery && visibleDbTypeItems.length === 0 ? (
<div
role="status"
style={{ ...modalMutedTextStyle, padding: "18px 10px" }}
>
{t("connection.modal.step1.search.empty")}
</div>
) : null}
</div>
</div>
</div>

View File

@@ -17,6 +17,10 @@ type LocalizedExpectation = {
};
const remainingConnectionModalSliceExpectations: LocalizedExpectation[] = [
{
key: "connection.modal.step1.group.all",
catalogKey: "connection_modal.step1.group.all",
},
{
key: "connection.modal.step1.group.relational",
catalogKey: "connection_modal.step1.group.relational",
@@ -69,6 +73,14 @@ const remainingConnectionModalSliceExpectations: LocalizedExpectation[] = [
key: "connection.modal.step1.hint.standard",
catalogKey: "connection_modal.step1.hint.standard",
},
{
key: "connection.modal.step1.search.empty",
catalogKey: "connection_modal.step1.search.empty",
},
{
key: "connection.modal.step1.search.placeholder",
catalogKey: "connection_modal.step1.search.placeholder",
},
{
key: "connection.modal.field.driver.label",
catalogKey: "connection_modal.field.driver_name",

View File

@@ -3767,6 +3767,7 @@
"connection_modal.status.unhealthy": "Fehlerhaft",
"connection_modal.step.select_source": "Datenquelle auswählen",
"connection_modal.step.select_source_description": "Wählen Sie den zu erstellenden Verbindungstyp.",
"connection_modal.step1.group.all": "Alle",
"connection_modal.step1.group.domestic": "Inländische Datenbanken",
"connection_modal.step1.group.message_queue": "Nachrichtenwarteschlangen",
"connection_modal.step1.group.nosql": "NoSQL-Datenbanken",
@@ -3786,6 +3787,8 @@
"connection_modal.step1.hint.qdrant": "Collection-Browsing, Vektorsuche und Payload-Filter",
"connection_modal.step1.hint.redis": "Einzelknoten / Cluster",
"connection_modal.step1.hint.standard": "Standard-Verbindungskonfiguration",
"connection_modal.step1.search.empty": "Keine passende Datenquelle",
"connection_modal.step1.search.placeholder": "Datenquellennamen suchen",
"connection_modal.switch.off": "Aus",
"connection_modal.switch.on": "Ein",
"connection_modal.table.health": "Zustand",

View File

@@ -3767,6 +3767,7 @@
"connection_modal.status.unhealthy": "Unhealthy",
"connection_modal.step.select_source": "Select data source",
"connection_modal.step.select_source_description": "Choose the connection type to create.",
"connection_modal.step1.group.all": "All",
"connection_modal.step1.group.domestic": "Domestic databases",
"connection_modal.step1.group.message_queue": "Message queues",
"connection_modal.step1.group.nosql": "NoSQL databases",
@@ -3786,6 +3787,8 @@
"connection_modal.step1.hint.qdrant": "Collection browsing, vector search, and Payload filtering",
"connection_modal.step1.hint.redis": "Single node / cluster",
"connection_modal.step1.hint.standard": "Standard connection configuration",
"connection_modal.step1.search.empty": "No matching data source",
"connection_modal.step1.search.placeholder": "Search data source name",
"connection_modal.switch.off": "Off",
"connection_modal.switch.on": "On",
"connection_modal.table.health": "Health",

View File

@@ -3767,6 +3767,7 @@
"connection_modal.status.unhealthy": "異常",
"connection_modal.step.select_source": "データソースを選択",
"connection_modal.step.select_source_description": "作成する接続タイプを選択します。",
"connection_modal.step1.group.all": "すべて",
"connection_modal.step1.group.domestic": "国産データベース",
"connection_modal.step1.group.message_queue": "メッセージキュー",
"connection_modal.step1.group.nosql": "NoSQL データベース",
@@ -3786,6 +3787,8 @@
"connection_modal.step1.hint.qdrant": "Collection 閲覧、ベクトル検索、Payload フィルター",
"connection_modal.step1.hint.redis": "シングルノード / クラスター",
"connection_modal.step1.hint.standard": "標準接続設定",
"connection_modal.step1.search.empty": "一致するデータソースがありません",
"connection_modal.step1.search.placeholder": "データソース名を検索",
"connection_modal.switch.off": "オフ",
"connection_modal.switch.on": "オン",
"connection_modal.table.health": "ヘルス",

View File

@@ -3767,6 +3767,7 @@
"connection_modal.status.unhealthy": "Неисправен",
"connection_modal.step.select_source": "Выбор источника данных",
"connection_modal.step.select_source_description": "Выберите тип создаваемого подключения.",
"connection_modal.step1.group.all": "Все",
"connection_modal.step1.group.domestic": "Отечественные базы данных",
"connection_modal.step1.group.message_queue": "Очереди сообщений",
"connection_modal.step1.group.nosql": "Базы данных NoSQL",
@@ -3786,6 +3787,8 @@
"connection_modal.step1.hint.qdrant": "Просмотр Collection, векторный поиск и фильтрация Payload",
"connection_modal.step1.hint.redis": "Одиночный узел / кластер",
"connection_modal.step1.hint.standard": "Стандартная конфигурация подключения",
"connection_modal.step1.search.empty": "Подходящих источников нет",
"connection_modal.step1.search.placeholder": "Поиск по названию источника",
"connection_modal.switch.off": "Выкл",
"connection_modal.switch.on": "Вкл",
"connection_modal.table.health": "Состояние",

View File

@@ -3767,6 +3767,7 @@
"connection_modal.status.unhealthy": "异常",
"connection_modal.step.select_source": "选择数据源",
"connection_modal.step.select_source_description": "选择要创建的连接类型。",
"connection_modal.step1.group.all": "全部",
"connection_modal.step1.group.domestic": "国产数据库",
"connection_modal.step1.group.message_queue": "消息队列",
"connection_modal.step1.group.nosql": "NoSQL 数据库",
@@ -3786,6 +3787,8 @@
"connection_modal.step1.hint.qdrant": "Collection 浏览、向量搜索和 Payload 过滤",
"connection_modal.step1.hint.redis": "单机 / 集群",
"connection_modal.step1.hint.standard": "标准连接配置",
"connection_modal.step1.search.empty": "没有匹配的数据源",
"connection_modal.step1.search.placeholder": "搜索数据源名称",
"connection_modal.switch.off": "关",
"connection_modal.switch.on": "开",
"connection_modal.table.health": "健康",

View File

@@ -3767,6 +3767,7 @@
"connection_modal.status.unhealthy": "異常",
"connection_modal.step.select_source": "選擇資料來源",
"connection_modal.step.select_source_description": "選擇要建立的連線類型。",
"connection_modal.step1.group.all": "全部",
"connection_modal.step1.group.domestic": "國產資料庫",
"connection_modal.step1.group.message_queue": "訊息佇列",
"connection_modal.step1.group.nosql": "NoSQL 資料庫",
@@ -3786,6 +3787,8 @@
"connection_modal.step1.hint.qdrant": "Collection 瀏覽、向量搜尋與 Payload 過濾",
"connection_modal.step1.hint.redis": "單機 / 叢集",
"connection_modal.step1.hint.standard": "標準連線設定",
"connection_modal.step1.search.empty": "沒有符合的資料來源",
"connection_modal.step1.search.placeholder": "搜尋資料來源名稱",
"connection_modal.switch.off": "關",
"connection_modal.switch.on": "開",
"connection_modal.table.health": "健康",