mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:49:30 +00:00
feat(redis): add configurable key search templates
This commit is contained in:
@@ -29,6 +29,7 @@ import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/l
|
||||
import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync";
|
||||
import { createConnectionNoteVisibilityDraft, persistConnectionNoteVisibilityDraft as persistConnectionNoteVisibilityDraftState, resetConnectionNoteVisibilityDraft, setConnectionNoteVisibilityDraft, syncConnectionNoteVisibilityDraft } from "./connectionNoteVisibilityDraft";
|
||||
import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern";
|
||||
import { normalizeRedisKeyTemplates, redisKeyTemplatesToTextarea } from "@/lib/redis/redisKeyTemplates";
|
||||
import { normalizeGlobalConnectTimeoutSecs, normalizeGlobalQueryTimeoutSecs, useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
@@ -363,6 +364,7 @@ const defaultForm = (): ConnectionForm => ({
|
||||
redis_cluster_nodes: "",
|
||||
redis_key_separator: ":",
|
||||
redis_scan_page_size: REDIS_SCAN_PAGE_SIZE_DEFAULT,
|
||||
redis_key_templates: [],
|
||||
etcd_endpoints: "",
|
||||
gbase_server: "",
|
||||
informix_server: "",
|
||||
@@ -556,6 +558,7 @@ function sshLayersForConfig(config: LegacyConnectionConfig): SshTunnelConfig[] {
|
||||
}
|
||||
|
||||
const form = ref(defaultForm());
|
||||
const redisKeyTemplatesText = ref("");
|
||||
const noteTextareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const showGaussdbConnectionMode = computed(() => form.value.db_type === "gaussdb");
|
||||
const gaussdbDriverMode = computed<GaussdbConnectionMode>({
|
||||
@@ -2484,6 +2487,7 @@ watch(
|
||||
redis_cluster_nodes: config.redis_cluster_nodes || "",
|
||||
redis_key_separator: config.redis_key_separator ?? ":",
|
||||
redis_scan_page_size: config.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT,
|
||||
redis_key_templates: normalizeRedisKeyTemplates(config.redis_key_templates),
|
||||
etcd_endpoints: config.etcd_endpoints || "",
|
||||
gbase_server: config.gbase_server || "",
|
||||
informix_server: config.informix_server || "",
|
||||
@@ -2499,6 +2503,7 @@ watch(
|
||||
visible_schemas: config.visible_schemas,
|
||||
save_password: config.save_password !== false,
|
||||
};
|
||||
redisKeyTemplatesText.value = redisKeyTemplatesToTextarea(form.value.redis_key_templates);
|
||||
oracleTnsAdminPath.value = parseOracleTnsConnectionString(config.connection_string)?.tnsAdmin || "";
|
||||
productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0;
|
||||
connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : "";
|
||||
@@ -2570,6 +2575,7 @@ watch(
|
||||
clearSavedDatabaseInfo();
|
||||
editingId.value = null;
|
||||
form.value = defaultForm();
|
||||
redisKeyTemplatesText.value = "";
|
||||
productionProtectionEnabled.value = false;
|
||||
selectedTransportLayerId.value = null;
|
||||
selectedType.value = "mysql";
|
||||
@@ -3859,6 +3865,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
config.redis_key_separator = undefined;
|
||||
config.redis_scan_page_size = undefined;
|
||||
config.redis_database_aliases = undefined;
|
||||
config.redis_key_templates = undefined;
|
||||
} else if (config.redis_connection_mode === "sentinel") {
|
||||
config.redis_sentinel_master = config.redis_sentinel_master?.trim() || "";
|
||||
config.redis_sentinel_nodes = normalizeRedisSentinelNodes(config.redis_sentinel_nodes || "");
|
||||
@@ -3894,6 +3901,11 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
config.redis_key_separator = config.redis_key_separator?.trim() ?? ":";
|
||||
const scanSize = Number(config.redis_scan_page_size);
|
||||
config.redis_scan_page_size = Number.isFinite(scanSize) && scanSize >= REDIS_SCAN_PAGE_SIZE_MIN && scanSize <= REDIS_SCAN_PAGE_SIZE_MAX ? Math.round(scanSize) : REDIS_SCAN_PAGE_SIZE_DEFAULT;
|
||||
{
|
||||
const templates = normalizeRedisKeyTemplates(redisKeyTemplatesText.value);
|
||||
config.redis_key_templates = templates.length > 0 ? templates : undefined;
|
||||
form.value.redis_key_templates = templates;
|
||||
}
|
||||
}
|
||||
if (config.db_type === "zookeeper") {
|
||||
const normalizedConnectString = normalizeZooKeeperConnectString(config.connection_string || "");
|
||||
@@ -4760,6 +4772,7 @@ function openJdbcDriverManagerFromError() {
|
||||
function resetForm(options: { preservePickerState?: boolean } = {}) {
|
||||
editingId.value = null;
|
||||
form.value = defaultForm();
|
||||
redisKeyTemplatesText.value = "";
|
||||
resetConnectionNoteVisibilityDraft(connectionNoteVisibilityDraft, settingsStore.editorSettings.sidebarShowConnectionNotes);
|
||||
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
|
||||
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
|
||||
@@ -6514,6 +6527,18 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.redisKeySeparator") }}</Label>
|
||||
<Input v-model="form.redis_key_separator" class="col-span-3 h-8 text-xs" placeholder=":" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelTopClass">{{ t("connection.redisKeyTemplates") }}</Label>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<textarea
|
||||
v-model="redisKeyTemplatesText"
|
||||
class="flex min-h-[76px] w-full rounded-md border border-input bg-transparent px-3 py-2 font-mono text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
:placeholder="t('connection.redisKeyTemplatesPlaceholder')"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">{{ t("connection.redisKeyTemplatesHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Consul KV: HTTP endpoint, ACL token and scope -->
|
||||
|
||||
@@ -106,6 +106,7 @@ import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keybo
|
||||
import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
|
||||
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
|
||||
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
|
||||
import { currentStatementFrameLayer } from "@/lib/editor/codemirrorCurrentStatementFrameLayer";
|
||||
import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers";
|
||||
@@ -456,6 +457,7 @@ const editFormatSqlOnSqlFileSave = ref(settingsStore.editorSettings.formatSqlOnS
|
||||
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
|
||||
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
|
||||
const editRedisKeyTemplates = ref(normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n"));
|
||||
const editSidebarObjectInfoMode = ref<SidebarObjectInfoMode>(settingsStore.editorSettings.sidebarObjectInfoMode);
|
||||
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
|
||||
const editSidebarIndent = ref(settingsStore.editorSettings.sidebarIndent);
|
||||
@@ -593,6 +595,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
sidebarIndent: editSidebarIndent.value,
|
||||
sidebarFontSize: editSidebarFontSize.value,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
|
||||
redisKeyTemplates: normalizeRedisKeyTemplates(editRedisKeyTemplates.value),
|
||||
exportBatchSize: editExportBatchSize.value,
|
||||
globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value,
|
||||
globalDateTimeExportFormat: editGlobalDateTimeExportFormat.value,
|
||||
@@ -882,6 +885,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
|
||||
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n");
|
||||
editSidebarObjectInfoMode.value = settingsStore.editorSettings.sidebarObjectInfoMode;
|
||||
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
|
||||
editSidebarIndent.value = settingsStore.editorSettings.sidebarIndent;
|
||||
@@ -1163,6 +1167,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editDuckDbWorkerProcessIsolation.value = DEFAULT_DESKTOP_SETTINGS.duckdb_worker_process_isolation;
|
||||
editDuckDbWorkerMaxProcesses.value = DEFAULT_DESKTOP_SETTINGS.duckdb_worker_max_processes;
|
||||
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields);
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
|
||||
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
|
||||
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
|
||||
editGlobalDateTimeExportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeExportFormat;
|
||||
@@ -1264,6 +1269,7 @@ function resetAllDefaults() {
|
||||
editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent;
|
||||
editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize;
|
||||
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
|
||||
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
|
||||
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
|
||||
editGlobalDateTimeExportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeExportFormat;
|
||||
@@ -5511,6 +5517,27 @@ onUnmounted(() => {
|
||||
|
||||
<Separator />
|
||||
|
||||
<div id="redis-key-templates" class="space-y-3">
|
||||
<div class="text-sm font-medium text-muted-foreground">
|
||||
{{ t("settings.redisKeyTemplatesSection") }}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="redis-key-templates-input">{{ t("settings.redisKeyTemplates") }}</Label>
|
||||
<textarea
|
||||
id="redis-key-templates-input"
|
||||
v-model="editRedisKeyTemplates"
|
||||
class="dbx-editor-font-family min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||
:placeholder="t('settings.redisKeyTemplatesPlaceholder')"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.redisKeyTemplatesDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium text-muted-foreground">
|
||||
{{ t("settings.exportSection") }}
|
||||
|
||||
@@ -1273,7 +1273,7 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
expect(document.body.textContent).toContain("first");
|
||||
});
|
||||
|
||||
it("cancels a sparse scan immediately when the search text changes", async () => {
|
||||
it("cancels a sparse scan immediately without searching until Enter", async () => {
|
||||
mocks.redisScanPageSize = 1_000;
|
||||
mountBrowser();
|
||||
await settle();
|
||||
@@ -1281,6 +1281,7 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
await settle();
|
||||
|
||||
const oldPage = deferred<{ cursor: number; keys: RedisKeyInfo[]; total_keys: number }>();
|
||||
const stale = { key_display: "old:result", key_raw: "b2xkOnJlc3VsdA==", key_type: "string", ttl: -1 };
|
||||
const fresh = { key_display: "new:result", key_raw: "bmV3OnJlc3VsdA==", key_type: "string", ttl: -1 };
|
||||
mocks.redisScanKeysBatch.mockReset();
|
||||
mocks.redisScanKeysBatch.mockImplementation((_connectionId: string, _db: number, _cursor: number, pattern: string) => {
|
||||
@@ -1291,14 +1292,22 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
await submitKeySearch("old");
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(1));
|
||||
|
||||
const input = requiredElement<HTMLInputElement>("[data-redis-search-input]");
|
||||
input.value = "new";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await settle();
|
||||
oldPage.resolve({ cursor: 1, keys: [], total_keys: 1_000_000 });
|
||||
await setInput("[data-redis-search-input]", "new");
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(1);
|
||||
mocks.updateRedisDbKeyStats.mockClear();
|
||||
|
||||
oldPage.resolve({ cursor: 0, keys: [stale], total_keys: 1 });
|
||||
await settle();
|
||||
|
||||
expect(mocks.updateRedisDbKeyStats).not.toHaveBeenCalled();
|
||||
expect(document.body.textContent).not.toContain("old:result");
|
||||
expect(document.body.textContent).not.toContain("new:result");
|
||||
expect(mocks.redisScanKeysBatch.mock.calls.filter((call) => call[3] === "*old*")).toHaveLength(1);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls.map((call) => call[3])).toEqual(["*old*"]);
|
||||
|
||||
requiredElement<HTMLInputElement>("[data-redis-search-input]").dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
await settle();
|
||||
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain("result"));
|
||||
expect(mocks.redisScanKeysBatch.mock.calls.map((call) => call[3])).toEqual(["*old*", "*new*"]);
|
||||
});
|
||||
@@ -1731,6 +1740,7 @@ describe("RedisKeyBrowser interrupted Fetch All", () => {
|
||||
it("does not select stale keys when a search changes during select-all scanning", async () => {
|
||||
const oldPage = deferred<{ cursor: number; keys: RedisKeyInfo[]; total_keys: number }>();
|
||||
const oldKey = { key_display: "old", key_raw: "b2xk", key_type: "string", ttl: -1 };
|
||||
const lateOldKey = { key_display: "late-old", key_raw: "bGF0ZS1vbGQ=", key_type: "string", ttl: -1 };
|
||||
const freshKey = { key_display: "fresh", key_raw: "ZnJlc2g=", key_type: "string", ttl: -1 };
|
||||
mocks.redisScanKeysBatch.mockImplementation((_connectionId: string, _db: number, cursor: number, pattern: string) => {
|
||||
if (pattern === "*" && cursor === 0) return Promise.resolve({ cursor: 1, keys: [oldKey], total_keys: 2 });
|
||||
@@ -1742,11 +1752,16 @@ describe("RedisKeyBrowser interrupted Fetch All", () => {
|
||||
|
||||
requiredElement<HTMLButtonElement>("[data-redis-select-all]").click();
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch.mock.calls.length).toBeGreaterThan(1));
|
||||
const input = requiredElement<HTMLInputElement>("[data-redis-search-input]");
|
||||
input.value = "fresh";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await setInput("[data-redis-search-input]", "fresh");
|
||||
oldPage.resolve({ cursor: 0, keys: [lateOldKey], total_keys: 0 });
|
||||
await settle();
|
||||
oldPage.resolve({ cursor: 0, keys: [oldKey], total_keys: 0 });
|
||||
|
||||
expect(document.body.textContent).not.toContain("late-old");
|
||||
expect(document.body.textContent).not.toContain("redis.stopFetchAll");
|
||||
expect(mocks.redisScanKeysBatch.mock.calls.every((call) => call[3] === "*")).toBe(true);
|
||||
expect(document.querySelector("[data-redis-batch-delete]")).toBeNull();
|
||||
|
||||
requiredElement<HTMLInputElement>("[data-redis-search-input]").dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
await settle();
|
||||
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain("fresh"));
|
||||
|
||||
@@ -52,6 +52,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { redisKeySearchPattern } from "@/lib/redis/redisKeyPattern";
|
||||
import { filterRedisKeyTemplates, resolveRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
|
||||
import { chunkRedisKeyRaws, collectUniqueRedisKeys } from "@/lib/redis/redisKeyBatch";
|
||||
import { getRedisCreateKeyTypeHelp, redisCreateKeyTypeHelpOptionOnOpen, shouldActivateRedisCreateKeyTypeHelpOnFocus } from "@/lib/redis/redisCreateKeyTypeHelp";
|
||||
@@ -112,6 +113,10 @@ const commandTerminalRef = ref<HTMLElement>();
|
||||
const searchPattern = ref("");
|
||||
const searchMode = ref<RedisSearchMode>("key");
|
||||
const fuzzyKeySearch = ref(false);
|
||||
const keyTemplateMenuOpen = ref(false);
|
||||
const keyTemplateSelectedIndex = ref(0);
|
||||
const keyTemplateListboxId = `redis-key-template-suggestions-${uuid()}`;
|
||||
let keyTemplateBlurTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const selectedKeyRaw = ref<string | null>(null);
|
||||
const hasMore = ref(false);
|
||||
const scanCursor = ref(0);
|
||||
@@ -216,6 +221,10 @@ const searchPlaceholder = computed(() => {
|
||||
if (searchMode.value === "key") return fuzzyKeySearch.value ? t("redis.fuzzyPattern") : t("redis.pattern");
|
||||
return searchMode.value === "all" ? t("redis.allSearchPlaceholder") : t("redis.valueSearchPlaceholder");
|
||||
});
|
||||
const redisKeyTemplates = computed(() => resolveRedisKeyTemplates(connectionStore.getConfig(props.connectionId)?.redis_key_templates, settingsStore.editorSettings.redisKeyTemplates ?? []));
|
||||
const keyTemplateSuggestions = computed(() => (searchMode.value === "key" ? filterRedisKeyTemplates(redisKeyTemplates.value, searchPattern.value) : []));
|
||||
const keyTemplateMenuVisible = computed(() => keyTemplateMenuOpen.value && searchMode.value === "key" && keyTemplateSuggestions.value.length > 0);
|
||||
const keyTemplateActiveDescendant = computed(() => (keyTemplateMenuVisible.value ? `${keyTemplateListboxId}-option-${keyTemplateSelectedIndex.value}` : undefined));
|
||||
const loadingEmptyText = computed(() => (isValueSearchMode.value && valueQuery.value ? t(searchMode.value === "all" ? "redis.searchingAll" : "redis.searchingValues") : t("redis.loadingKeys")));
|
||||
const redisKeySeparator = computed(() => connectionStore.getConfig(props.connectionId)?.redis_key_separator ?? ":");
|
||||
const redisScanPageSize = computed(() => connectionStore.getConfig(props.connectionId)?.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT);
|
||||
@@ -1636,7 +1645,90 @@ function typeColor(type: string): string {
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let hasAutoFocusedSearch = false;
|
||||
|
||||
function dismissKeyTemplateMenu() {
|
||||
keyTemplateMenuOpen.value = false;
|
||||
keyTemplateSelectedIndex.value = 0;
|
||||
if (keyTemplateBlurTimer) {
|
||||
clearTimeout(keyTemplateBlurTimer);
|
||||
keyTemplateBlurTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openKeyTemplateMenu() {
|
||||
if (searchMode.value !== "key" || redisKeyTemplates.value.length === 0) {
|
||||
dismissKeyTemplateMenu();
|
||||
return;
|
||||
}
|
||||
keyTemplateMenuOpen.value = true;
|
||||
keyTemplateSelectedIndex.value = 0;
|
||||
}
|
||||
|
||||
function clampKeyTemplateSelection() {
|
||||
const max = Math.max(keyTemplateSuggestions.value.length - 1, 0);
|
||||
if (keyTemplateSelectedIndex.value > max) keyTemplateSelectedIndex.value = max;
|
||||
}
|
||||
|
||||
function selectKeyTemplate(index: number) {
|
||||
const template = keyTemplateSuggestions.value[index];
|
||||
if (!template) return;
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = null;
|
||||
}
|
||||
searchPending.value = false;
|
||||
searchPattern.value = template;
|
||||
dismissKeyTemplateMenu();
|
||||
void nextTick(() => getSearchInput()?.focus());
|
||||
}
|
||||
|
||||
function moveKeyTemplateSelection(direction: number): boolean {
|
||||
if (!keyTemplateMenuVisible.value) return false;
|
||||
const count = keyTemplateSuggestions.value.length;
|
||||
if (count === 0) return false;
|
||||
keyTemplateSelectedIndex.value = Math.min(Math.max(keyTemplateSelectedIndex.value + direction, 0), count - 1);
|
||||
void nextTick(() => {
|
||||
document.getElementById(`${keyTemplateListboxId}-option-${keyTemplateSelectedIndex.value}`)?.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function onSearchFocus() {
|
||||
openKeyTemplateMenu();
|
||||
}
|
||||
|
||||
function onSearchBlur() {
|
||||
if (keyTemplateBlurTimer) clearTimeout(keyTemplateBlurTimer);
|
||||
keyTemplateBlurTimer = setTimeout(() => {
|
||||
dismissKeyTemplateMenu();
|
||||
keyTemplateBlurTimer = null;
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function onSearchInput() {
|
||||
if (searchMode.value === "key" && redisKeyTemplates.value.length > 0) {
|
||||
keyTemplateMenuOpen.value = true;
|
||||
keyTemplateSelectedIndex.value = 0;
|
||||
clampKeyTemplateSelection();
|
||||
} else {
|
||||
dismissKeyTemplateMenu();
|
||||
}
|
||||
// Key search is Enter-only so users can finish editing templates / patterns
|
||||
// (including {$placeholders}) without triggering SCAN on every keystroke.
|
||||
if (searchMode.value === "key") {
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = null;
|
||||
}
|
||||
searchPending.value = false;
|
||||
invalidateScanRequests();
|
||||
loading.value = false;
|
||||
isFetchingAll.value = false;
|
||||
fetchAllStopRequested.value = true;
|
||||
fetchAllLoadedCount.value = 0;
|
||||
selectedKeyRaw.value = null;
|
||||
resetCheckedKeys();
|
||||
return;
|
||||
}
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
// Invalidate in-flight SCAN work as soon as the query changes instead of
|
||||
// waiting for the debounce timer to start the replacement search.
|
||||
@@ -1654,6 +1746,7 @@ function onSearchInput() {
|
||||
function setSearchMode(mode: RedisSearchMode) {
|
||||
if (searchMode.value === mode) return;
|
||||
searchMode.value = mode;
|
||||
if (mode !== "key") dismissKeyTemplateMenu();
|
||||
void loadKeys();
|
||||
}
|
||||
|
||||
@@ -1834,6 +1927,28 @@ function focusSearch(): boolean {
|
||||
}
|
||||
|
||||
function onSearchKeydown(event: KeyboardEvent) {
|
||||
if (keyTemplateMenuVisible.value) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
moveKeyTemplateSelection(1);
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
moveKeyTemplateSelection(-1);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
selectKeyTemplate(keyTemplateSelectedIndex.value);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
dismissKeyTemplateMenu();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
void loadKeys();
|
||||
return;
|
||||
@@ -1841,6 +1956,7 @@ function onSearchKeydown(event: KeyboardEvent) {
|
||||
if (!isCancelSearchShortcut(event)) return;
|
||||
event.preventDefault();
|
||||
searchPattern.value = "";
|
||||
dismissKeyTemplateMenu();
|
||||
void loadKeys();
|
||||
}
|
||||
|
||||
@@ -2083,15 +2199,37 @@ defineExpose({ focusSearch, insertCommand, executeCommand: executeAiCommand });
|
||||
</div>
|
||||
<div class="redis-key-search-row mt-2">
|
||||
<div class="relative min-w-0">
|
||||
<Search class="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/80" />
|
||||
<Search class="pointer-events-none absolute left-2.5 top-1/2 z-[1] h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/80" />
|
||||
<Input
|
||||
v-model="searchPattern"
|
||||
data-redis-search-input
|
||||
role="combobox"
|
||||
class="h-8 border-border/70 bg-background pl-8 pr-3 text-xs shadow-sm caret-primary placeholder:text-muted-foreground/80 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-primary/20"
|
||||
:placeholder="searchPlaceholder"
|
||||
:aria-expanded="keyTemplateMenuVisible"
|
||||
:aria-controls="keyTemplateMenuVisible ? keyTemplateListboxId : undefined"
|
||||
:aria-activedescendant="keyTemplateActiveDescendant"
|
||||
autocomplete="off"
|
||||
@input="onSearchInput"
|
||||
@keydown="onSearchKeydown"
|
||||
@focus="onSearchFocus"
|
||||
@blur="onSearchBlur"
|
||||
/>
|
||||
<div v-if="keyTemplateMenuVisible" :id="keyTemplateListboxId" role="listbox" :aria-label="t('redis.keyTemplateSuggestions')" class="absolute left-0 right-0 top-[calc(100%+0.25rem)] z-30 max-h-60 overflow-y-auto rounded-md border bg-popover py-1 text-popover-foreground shadow-md">
|
||||
<button
|
||||
v-for="(template, index) in keyTemplateSuggestions"
|
||||
:id="`${keyTemplateListboxId}-option-${index}`"
|
||||
:key="template"
|
||||
type="button"
|
||||
role="option"
|
||||
class="dbx-editor-font-family flex w-full items-center px-3 py-1.5 text-left text-xs hover:bg-accent hover:text-accent-foreground"
|
||||
:class="keyTemplateSelectedIndex === index ? 'bg-accent text-accent-foreground' : ''"
|
||||
:aria-selected="keyTemplateSelectedIndex === index"
|
||||
@mousedown.prevent="selectKeyTemplate(index)"
|
||||
>
|
||||
<span class="truncate">{{ template }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<Button
|
||||
|
||||
@@ -619,6 +619,9 @@ export default {
|
||||
redisSentinelTls: "Sentinel TLS",
|
||||
redisSentinelTlsHint: "Use TLS when connecting to Sentinel nodes",
|
||||
redisKeySeparator: "Key Namespace Separator",
|
||||
redisKeyTemplates: "Key search templates",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "One template per line. When set, these replace the global Redis key templates for this connection.",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "One endpoint per line. Leave blank to use the host and port above.",
|
||||
etcdGrpcMaxInbound: "gRPC receive limit (MiB)",
|
||||
@@ -4536,6 +4539,7 @@ export default {
|
||||
noKeysInScanHint: "No keys found in current scan range",
|
||||
pattern: "pattern (e.g. user:*)",
|
||||
fuzzyPattern: "key contains...",
|
||||
keyTemplateSuggestions: "Key templates",
|
||||
fuzzyMatch: "Fuzzy",
|
||||
fuzzyMatchTitle: "Fuzzy match: search keys by plain text contains",
|
||||
valueSearchPlaceholder: "value contains...",
|
||||
@@ -6490,6 +6494,10 @@ export default {
|
||||
sqlVariableSyntax_atSetDescription: "Expand inline variable declarations and substitute their values before running.",
|
||||
autoAliasTables: "Automatically add table aliases",
|
||||
autoAliasTablesDescription: "When selecting a table completion in FROM or JOIN, insert a generated alias such as order_items AS oi.",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "Key search templates",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "One template per line. Used as fallback suggestions when a Redis connection has no templates of its own.",
|
||||
redisScanPageSize: "Redis scan count",
|
||||
redisScanPageSizeDescription: "Keys requested per Redis SCAN page when browsing keys.",
|
||||
redisScanPageSizeOption: "{count} keys",
|
||||
|
||||
@@ -594,6 +594,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "TLS Sentinel",
|
||||
redisSentinelTlsHint: "Usar TLS al conectar con nodos Sentinel",
|
||||
redisKeySeparator: "Separador de espacio de nombres de clave",
|
||||
redisKeyTemplates: "Plantillas de búsqueda de claves",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "Una plantilla por línea. Si se define, reemplaza las plantillas globales para esta conexión.",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "Una entrada por línea. Déjalo en blanco para usar el host y puerto de arriba.",
|
||||
etcdGrpcMaxInbound: "Límite de recepción gRPC (MiB)",
|
||||
@@ -4361,6 +4364,7 @@ export default withEnglishFallback({
|
||||
noKeysInScanHint: "No se encontraron claves en el rango de escaneo actual",
|
||||
pattern: "patrón (p. ej. usuario:*)",
|
||||
fuzzyPattern: "la clave contiene...",
|
||||
keyTemplateSuggestions: "Plantillas de clave",
|
||||
fuzzyMatch: "Difusa",
|
||||
fuzzyMatchTitle: "Coincidencia difusa: busca claves que contengan texto plano",
|
||||
valueSearchPlaceholder: "el valor contiene...",
|
||||
@@ -6172,6 +6176,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "Expande las declaraciones de variables en línea y sustituye sus valores antes de ejecutar.",
|
||||
autoAliasTables: "Agregar alias de tabla automáticamente",
|
||||
autoAliasTablesDescription: "Al seleccionar una tabla en FROM o JOIN, inserta un alias generado como order_items AS oi.",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "Plantillas de búsqueda de claves",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "Una plantilla por línea. Respaldo cuando la conexión no tiene plantillas propias.",
|
||||
redisScanPageSize: "Cantidad de escaneo Redis",
|
||||
redisScanPageSizeDescription: "Claves solicitadas por página SCAN al explorar claves Redis.",
|
||||
redisScanPageSizeOption: "{count} claves",
|
||||
|
||||
@@ -593,6 +593,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "TLS Sentinel",
|
||||
redisSentinelTlsHint: "Usa TLS quando ti connetti ai nodi Sentinel",
|
||||
redisKeySeparator: "Separatore namespace chiavi",
|
||||
redisKeyTemplates: "Modelli di ricerca chiavi",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "Un modello per riga. Se impostato, sostituisce i modelli globali per questa connessione.",
|
||||
etcdEndpoints: "Endpoint",
|
||||
etcdEndpointsHint: "Un endpoint per riga. Lascia vuoto per usare l'host e la porta sopra.",
|
||||
etcdGrpcMaxInbound: "Limite ricezione gRPC (MiB)",
|
||||
@@ -4359,6 +4362,7 @@ export default withEnglishFallback({
|
||||
noKeysInScanHint: "Nessuna chiave trovata nell'intervallo di scansione corrente",
|
||||
pattern: "pattern (es. user:*)",
|
||||
fuzzyPattern: "la chiave contiene...",
|
||||
keyTemplateSuggestions: "Modelli di chiave",
|
||||
fuzzyMatch: "Fuzzy",
|
||||
fuzzyMatchTitle: "Corrispondenza fuzzy: cerca le chiavi per testo semplice contenuto",
|
||||
valueSearchPlaceholder: "il valore contiene...",
|
||||
@@ -6172,6 +6176,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "Espande le dichiarazioni di variabili inline e ne sostituisce i valori prima dell'esecuzione.",
|
||||
autoAliasTables: "Aggiungi alias tabella automaticamente",
|
||||
autoAliasTablesDescription: "Quando scegli una tabella in FROM o JOIN, inserisce un alias generato come order_items AS oi.",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "Modelli di ricerca chiavi",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "Un modello per riga. Fallback quando la connessione non ha modelli propri.",
|
||||
redisScanPageSize: "Conteggio scansione Redis",
|
||||
redisScanPageSizeDescription: "Chiavi richieste per pagina SCAN di Redis durante la navigazione delle chiavi.",
|
||||
redisScanPageSizeOption: "{count} chiavi",
|
||||
|
||||
@@ -593,6 +593,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "センチネルTLS",
|
||||
redisSentinelTlsHint: "センチネルノード接続時にTLSを使用する",
|
||||
redisKeySeparator: "キー名前空間セパレーター",
|
||||
redisKeyTemplates: "キー検索テンプレート",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "1行に1つのテンプレート。設定するとこの接続のグローバルテンプレートを上書きします。",
|
||||
etcdEndpoints: "エンドポイント",
|
||||
etcdEndpointsHint: "1行に1つのエンドポイント。空白の場合、上のホストとポートを使用します。",
|
||||
etcdGrpcMaxInbound: "gRPC 受信上限(MiB)",
|
||||
@@ -4387,6 +4390,7 @@ export default withEnglishFallback({
|
||||
noKeys: "キーが見つかりません",
|
||||
pattern: "パターン(例: user:*)",
|
||||
fuzzyPattern: "キーを含む...",
|
||||
keyTemplateSuggestions: "キーテンプレート",
|
||||
fuzzyMatch: "あいまい",
|
||||
fuzzyMatchTitle: "あいまい検索: プレーンテキストの部分一致でキーを検索",
|
||||
valueSearchPlaceholder: "値を含む...",
|
||||
@@ -6190,6 +6194,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "実行前にインライン変数宣言を展開し、その値をインライン化します。",
|
||||
autoAliasTables: "テーブル別名を自動追加",
|
||||
autoAliasTablesDescription: "FROM または JOIN でテーブル補完を選択すると、order_items AS oi のような生成済み別名を挿入します。",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "キー検索テンプレート",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "1行に1つのテンプレート。接続にテンプレートがない場合のフォールバックです。",
|
||||
redisScanPageSize: "Redisスキャンカウント",
|
||||
redisScanPageSizeDescription: "キー参照時のRedis SCANページあたりの要求キー数。",
|
||||
redisScanPageSizeOption: "{count}キー",
|
||||
|
||||
@@ -613,6 +613,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "센티널 TLS",
|
||||
redisSentinelTlsHint: "센티널 노드에 연결할 때 TLS 사용",
|
||||
redisKeySeparator: "키 네임스페이스 구분자",
|
||||
redisKeyTemplates: "키 검색 템플릿",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "한 줄에 하나의 템플릿. 설정하면 이 연결의 전역 템플릿을 덮어씁니다.",
|
||||
etcdEndpoints: "엔드포인트",
|
||||
etcdEndpointsHint: "한 줄에 하나의 엔드포인트. 비워두면 위의 호스트와 포트를 사용합니다.",
|
||||
etcdGrpcMaxInbound: "gRPC 수신 제한 (MiB)",
|
||||
@@ -3972,6 +3975,7 @@ export default withEnglishFallback({
|
||||
noKeysInScanHint: "현재 스캔 범위에 키가 없습니다",
|
||||
pattern: "패턴 (예: user:*)",
|
||||
fuzzyPattern: "키에 포함된...",
|
||||
keyTemplateSuggestions: "키 템플릿",
|
||||
fuzzyMatch: "퍼지",
|
||||
fuzzyMatchTitle: "퍼지 일치: 일반 텍스트 포함으로 키 검색",
|
||||
valueSearchPlaceholder: "값에 포함된...",
|
||||
@@ -5873,6 +5877,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "실행 전에 인라인 변수 선언을 확장하고 값을 치환합니다.",
|
||||
autoAliasTables: "자동으로 테이블 별칭 추가",
|
||||
autoAliasTablesDescription: "FROM이나 JOIN에서 테이블 완성을 선택할 때 order_items AS oi 같은 생성된 별칭을 삽입합니다.",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "키 검색 템플릿",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "한 줄에 하나의 템플릿. 연결에 템플릿이 없을 때 전역 폴백으로 사용됩니다.",
|
||||
redisScanPageSize: "Redis 스캔 개수",
|
||||
redisScanPageSizeDescription: "키를 탐색할 때 Redis SCAN 페이지당 요청하는 키 수입니다.",
|
||||
redisScanPageSizeOption: "키 {count}개",
|
||||
|
||||
@@ -594,6 +594,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "TLS do Sentinel",
|
||||
redisSentinelTlsHint: "Usar TLS ao conectar aos nós Sentinel",
|
||||
redisKeySeparator: "Separador de namespace de chave",
|
||||
redisKeyTemplates: "Modelos de busca de chave",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "Um modelo por linha. Quando definido, substitui os modelos globais nesta conexão.",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "Um endpoint por linha. Deixe em branco para usar o host e porta acima.",
|
||||
etcdGrpcMaxInbound: "Limite de recebimento gRPC (MiB)",
|
||||
@@ -4361,6 +4364,7 @@ export default withEnglishFallback({
|
||||
noKeysInScanHint: "Nenhuma chave encontrada no intervalo atual de verificação",
|
||||
pattern: "padrão (ex.: user:*)",
|
||||
fuzzyPattern: "a chave contém...",
|
||||
keyTemplateSuggestions: "Modelos de chave",
|
||||
fuzzyMatch: "Aproximado",
|
||||
fuzzyMatchTitle: "Correspondência aproximada: pesquisa chaves por texto contido",
|
||||
valueSearchPlaceholder: "o valor contém...",
|
||||
@@ -6174,6 +6178,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "Expande as declarações de variáveis inline e substitui seus valores antes de executar.",
|
||||
autoAliasTables: "Adicionar alias de tabela automaticamente",
|
||||
autoAliasTablesDescription: "Ao escolher uma tabela em FROM ou JOIN, insere um alias gerado como order_items AS oi.",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "Modelos de busca de chave",
|
||||
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "Um modelo por linha. Fallback quando a conexão não tem modelos próprios.",
|
||||
redisScanPageSize: "Contagem de scan do Redis",
|
||||
redisScanPageSizeDescription: "Chaves solicitadas por página do Redis SCAN ao navegar pelas chaves.",
|
||||
redisScanPageSizeOption: "{count} chaves",
|
||||
|
||||
@@ -544,6 +544,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "哨兵 TLS",
|
||||
redisSentinelTlsHint: "连接 Sentinel 节点时使用 TLS",
|
||||
redisKeySeparator: "键名分隔符(可选)",
|
||||
redisKeyTemplates: "Key 搜索模板",
|
||||
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "每行一个模板。填写后将覆盖全局 Redis Key 模板,仅对本连接生效。",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "每行一个 endpoint。留空时使用上面的 host 和端口。",
|
||||
etcdGrpcMaxInbound: "gRPC 接收上限(MiB)",
|
||||
@@ -4521,6 +4524,7 @@ export default withEnglishFallback({
|
||||
noKeysInScanHint: "当前扫描范围内未命中匹配的 key",
|
||||
pattern: "匹配模式 (如 user:*)",
|
||||
fuzzyPattern: "输入关键字搜索 key",
|
||||
keyTemplateSuggestions: "Key 模板",
|
||||
fuzzyMatch: "模糊",
|
||||
fuzzyMatchTitle: "模糊匹配:自动按包含关系搜索 key",
|
||||
valueSearchPlaceholder: "按值内容搜索...",
|
||||
@@ -6473,6 +6477,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "执行前展开内联变量声明并就地代入其值。",
|
||||
autoAliasTables: "自动添加表别名",
|
||||
autoAliasTablesDescription: "在 FROM 或 JOIN 中选择表名补全时,自动插入类似 order_items AS oi 的表别名。",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "Key 搜索模板",
|
||||
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "每行一个模板。当连接未配置模板时,作为全局回退建议。",
|
||||
redisScanPageSize: "Redis 扫描数量",
|
||||
redisScanPageSizeDescription: "浏览 Redis Key 时每次 SCAN 请求的 Key 数量。",
|
||||
redisScanPageSizeOption: "{count} 个 Key",
|
||||
|
||||
@@ -594,6 +594,9 @@ export default withEnglishFallback({
|
||||
redisSentinelTls: "Sentinel TLS",
|
||||
redisSentinelTlsHint: "連線 Sentinel 節點時使用 TLS",
|
||||
redisKeySeparator: "鍵名分隔符(可選)",
|
||||
redisKeyTemplates: "Key 搜尋模板",
|
||||
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesHint: "每行一個模板。設定後會覆寫全域 Redis Key 模板,僅對此連線生效。",
|
||||
etcdEndpoints: "端點",
|
||||
etcdEndpointsHint: "每行一個端點。留空則使用上方的主機和連接埠。",
|
||||
etcdGrpcMaxInbound: "gRPC 接收上限(MiB)",
|
||||
@@ -3685,6 +3688,7 @@ export default withEnglishFallback({
|
||||
noKeysInScanHint: "目前掃描範圍內未找到匹配的 key",
|
||||
pattern: "模式 (例如 user:*)",
|
||||
fuzzyPattern: "輸入關鍵字搜尋 key",
|
||||
keyTemplateSuggestions: "Key 模板",
|
||||
fuzzyMatch: "模糊",
|
||||
fuzzyMatchTitle: "模糊匹配:自動按包含關係搜尋 key",
|
||||
valueSearchPlaceholder: "值包含……",
|
||||
@@ -5490,6 +5494,10 @@ export default withEnglishFallback({
|
||||
sqlVariableSyntax_atSetDescription: "執行前展開內聯變數宣告並就地代入其值。",
|
||||
autoAliasTables: "自動加入資料表別名",
|
||||
autoAliasTablesDescription: "在 FROM 或 JOIN 中選擇資料表補全時,自動插入類似 order_items AS oi 的資料表別名。",
|
||||
redisKeyTemplatesSection: "Redis",
|
||||
redisKeyTemplates: "Key 搜尋模板",
|
||||
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
|
||||
redisKeyTemplatesDescription: "每行一個模板。當連線未設定模板時,作為全域後備建議。",
|
||||
redisScanPageSize: "Redis 掃描數量",
|
||||
redisScanPageSizeDescription: "瀏覽 Redis Key 時每次 SCAN 請求的 Key 數量。",
|
||||
redisScanPageSizeOption: "{count} 個 Key",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterRedisKeyTemplates, normalizeRedisKeyTemplates, redisKeyTemplatesToTextarea, resolveRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
|
||||
describe("redisKeyTemplates", () => {
|
||||
it("normalizes arrays and newline text, trimming and deduping", () => {
|
||||
expect(normalizeRedisKeyTemplates([" user:{$id} ", "", "v3:adui:{$positionID}", "user:{$id}", " "])).toEqual(["user:{$id}", "v3:adui:{$positionID}"]);
|
||||
|
||||
expect(normalizeRedisKeyTemplates("user:{$id}\n\nv3:adui\nuser:{$id}\n")).toEqual(["user:{$id}", "v3:adui"]);
|
||||
expect(normalizeRedisKeyTemplates(null)).toEqual([]);
|
||||
expect(normalizeRedisKeyTemplates(undefined)).toEqual([]);
|
||||
expect(normalizeRedisKeyTemplates([1, "ok"] as unknown[])).toEqual(["ok"]);
|
||||
});
|
||||
|
||||
it("resolves connection templates over global when connection list is non-empty", () => {
|
||||
expect(resolveRedisKeyTemplates(["conn:a"], ["global:a", "global:b"])).toEqual(["conn:a"]);
|
||||
expect(resolveRedisKeyTemplates([], ["global:a"])).toEqual(["global:a"]);
|
||||
expect(resolveRedisKeyTemplates(undefined, ["global:a"])).toEqual(["global:a"]);
|
||||
expect(resolveRedisKeyTemplates([" ", ""], ["global:a"])).toEqual(["global:a"]);
|
||||
expect(resolveRedisKeyTemplates(undefined, undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it("filters by case-insensitive substring and returns all when query is blank", () => {
|
||||
const templates = ["v3:adui:{$positionID}", "v4:adui:creative_feed:{$arg1}", "cache:session"];
|
||||
expect(filterRedisKeyTemplates(templates, "")).toEqual(templates);
|
||||
expect(filterRedisKeyTemplates(templates, " ")).toEqual(templates);
|
||||
expect(filterRedisKeyTemplates(templates, "ADUI")).toEqual(["v3:adui:{$positionID}", "v4:adui:creative_feed:{$arg1}"]);
|
||||
expect(filterRedisKeyTemplates(templates, "session")).toEqual(["cache:session"]);
|
||||
expect(filterRedisKeyTemplates(templates, "missing")).toEqual([]);
|
||||
});
|
||||
|
||||
it("joins templates for textarea editing", () => {
|
||||
expect(redisKeyTemplatesToTextarea(["a", "b"])).toBe("a\nb");
|
||||
expect(redisKeyTemplatesToTextarea(undefined)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Helpers for configurable Redis key-search templates.
|
||||
* Connection-level templates override the global list when non-empty.
|
||||
*/
|
||||
|
||||
export function normalizeRedisKeyTemplates(value: unknown): string[] {
|
||||
const rawLines = typeof value === "string" ? value.split(/\r?\n/) : Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
||||
|
||||
const seen = new Set<string>();
|
||||
const templates: string[] = [];
|
||||
for (const raw of rawLines) {
|
||||
const template = raw.trim();
|
||||
if (!template || seen.has(template)) continue;
|
||||
seen.add(template);
|
||||
templates.push(template);
|
||||
}
|
||||
return templates;
|
||||
}
|
||||
|
||||
/** Empty connection list means inherit global (not “no templates”). */
|
||||
export function resolveRedisKeyTemplates(connectionTemplates: string[] | undefined | null, globalTemplates: string[] | undefined | null): string[] {
|
||||
const fromConnection = normalizeRedisKeyTemplates(connectionTemplates);
|
||||
if (fromConnection.length > 0) return fromConnection;
|
||||
return normalizeRedisKeyTemplates(globalTemplates);
|
||||
}
|
||||
|
||||
export function filterRedisKeyTemplates(templates: readonly string[], query: string): string[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [...templates];
|
||||
return templates.filter((template) => template.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
export function redisKeyTemplatesToTextarea(templates: readonly string[] | undefined | null): string {
|
||||
return normalizeRedisKeyTemplates(templates).join("\n");
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { EditorSettings } from "@/stores/settingsStore";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
|
||||
export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"fontFamily",
|
||||
@@ -76,6 +77,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"sidebarIndent",
|
||||
"sidebarFontSize",
|
||||
"sidebarHiddenTablePrefixes",
|
||||
"redisKeyTemplates",
|
||||
"exportBatchSize",
|
||||
"exportRowLimitEnabled",
|
||||
"exportRowLimit",
|
||||
@@ -114,6 +116,7 @@ function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unkn
|
||||
if (key === "pageSize" || key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value);
|
||||
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
|
||||
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
|
||||
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "data-grid-compact-header-actions", category: "data", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "data" },
|
||||
{ id: "data-grid-auto-total", category: "data", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "data" },
|
||||
{ id: "data-grid-infinite-scroll", category: "data", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "data" },
|
||||
{ id: "redis-key-templates", category: "data", titleKey: "settings.redisKeyTemplates", descriptionKey: "settings.redisKeyTemplatesDescription", targetId: "redis-key-templates" },
|
||||
{ id: "data-grid-auto-transpose", category: "data", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "data" },
|
||||
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
|
||||
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data" },
|
||||
|
||||
@@ -139,6 +139,7 @@ import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/metad
|
||||
import { kvRootNodeLabel } from "@/lib/kv/kvRootPresentation";
|
||||
import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
|
||||
import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVersion, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
|
||||
import { appendConnectionErrorHints, isMysqlMissingPasswordFailure } from "@/lib/connection/connectionErrorHints";
|
||||
import { connectionNeedsPasswordPrompt } from "@/lib/connection/connectionPassword";
|
||||
@@ -1259,6 +1260,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
idle_timeout_secs: config.idle_timeout_secs ?? 60,
|
||||
keepalive_interval_secs: config.keepalive_interval_secs ?? DEFAULT_KEEPALIVE_INTERVAL_SECS,
|
||||
redis_database_aliases: normalizeRedisDatabaseAliases(config.redis_database_aliases),
|
||||
redis_key_templates: (() => {
|
||||
const templates = normalizeRedisKeyTemplates(config.redis_key_templates);
|
||||
return templates.length > 0 ? templates : undefined;
|
||||
})(),
|
||||
database_info: normalizeDatabaseConnectionInfo(config.database_info),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings,
|
||||
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
|
||||
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
|
||||
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
|
||||
import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
|
||||
@@ -621,6 +622,8 @@ export interface EditorSettings {
|
||||
snippets: SqlSnippet[];
|
||||
tableColumnTemplateFields: string[];
|
||||
exportBatchSize: number;
|
||||
/** Global Redis key-search templates; overridden by non-empty connection templates. */
|
||||
redisKeyTemplates: string[];
|
||||
exportRowLimitEnabled: boolean;
|
||||
exportRowLimit: number;
|
||||
queryExportKeysetOptimizationEnabled: boolean;
|
||||
@@ -832,6 +835,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
snippets: DEFAULT_SQL_SNIPPETS,
|
||||
tableColumnTemplateFields: [...DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS],
|
||||
exportBatchSize: 2000,
|
||||
redisKeyTemplates: [],
|
||||
exportRowLimitEnabled: false,
|
||||
exportRowLimit: 100000,
|
||||
queryExportKeysetOptimizationEnabled: true,
|
||||
@@ -1246,6 +1250,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
snippets: normalizeSqlSnippets(settings.snippets, existing?.snippets),
|
||||
tableColumnTemplateFields: normalizeTableColumnTemplateFields(settings.tableColumnTemplateFields),
|
||||
exportBatchSize: typeof settings.exportBatchSize === "number" && settings.exportBatchSize >= 100 && settings.exportBatchSize <= 100000 ? Math.round(settings.exportBatchSize) : DEFAULT_EDITOR_SETTINGS.exportBatchSize,
|
||||
redisKeyTemplates: normalizeRedisKeyTemplates(settings.redisKeyTemplates),
|
||||
exportRowLimitEnabled: typeof settings.exportRowLimitEnabled === "boolean" ? settings.exportRowLimitEnabled : DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled,
|
||||
exportRowLimit: typeof settings.exportRowLimit === "number" && settings.exportRowLimit >= 100 && settings.exportRowLimit <= 2147483647 ? Math.round(settings.exportRowLimit) : DEFAULT_EDITOR_SETTINGS.exportRowLimit,
|
||||
queryExportKeysetOptimizationEnabled: typeof settings.queryExportKeysetOptimizationEnabled === "boolean" ? settings.queryExportKeysetOptimizationEnabled : DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled,
|
||||
@@ -1823,6 +1828,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.snippets !== undefined) editorSettings.value.snippets = normalizeSqlSnippets(partial.snippets);
|
||||
if (partial.tableColumnTemplateFields !== undefined) editorSettings.value.tableColumnTemplateFields = normalizeTableColumnTemplateFields(partial.tableColumnTemplateFields);
|
||||
if (partial.exportBatchSize !== undefined) editorSettings.value.exportBatchSize = Math.min(100000, Math.max(100, Math.round(partial.exportBatchSize)));
|
||||
if (partial.redisKeyTemplates !== undefined) editorSettings.value.redisKeyTemplates = normalizeRedisKeyTemplates(partial.redisKeyTemplates);
|
||||
if (partial.exportRowLimitEnabled !== undefined) editorSettings.value.exportRowLimitEnabled = partial.exportRowLimitEnabled;
|
||||
if (partial.exportRowLimit !== undefined) editorSettings.value.exportRowLimit = Math.min(2147483647, Math.max(100, Math.round(partial.exportRowLimit)));
|
||||
if (partial.queryExportKeysetOptimizationEnabled !== undefined) editorSettings.value.queryExportKeysetOptimizationEnabled = partial.queryExportKeysetOptimizationEnabled;
|
||||
|
||||
@@ -115,6 +115,8 @@ export interface ConnectionConfig {
|
||||
redis_key_separator?: string;
|
||||
redis_scan_page_size?: number;
|
||||
redis_database_aliases?: Record<string, string>;
|
||||
/** Key-search templates for the Redis browser. Non-empty overrides global settings. */
|
||||
redis_key_templates?: string[];
|
||||
etcd_endpoints?: string;
|
||||
gbase_server?: string;
|
||||
informix_server?: string;
|
||||
|
||||
@@ -197,6 +197,7 @@ fn connection_config(id: &str, database: BenchDatabase) -> Result<ConnectionConf
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -738,6 +738,7 @@ mod tests {
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -1340,6 +1340,7 @@ for line in sys.stdin:
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -1797,6 +1797,7 @@ mod tests {
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
@@ -1857,6 +1858,7 @@ mod tests {
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
@@ -2115,6 +2117,7 @@ mod tests {
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -5772,6 +5772,7 @@ mod tests {
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -867,6 +867,7 @@ mod tests {
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -5632,6 +5632,7 @@ mod tests {
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -153,6 +153,10 @@ pub struct ConnectionConfig {
|
||||
pub redis_scan_page_size: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub redis_database_aliases: HashMap<String, String>,
|
||||
/// Optional key-search templates for the Redis key browser (one pattern per entry).
|
||||
/// Empty means inherit the global editor setting.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub redis_key_templates: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub etcd_endpoints: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
@@ -577,6 +581,8 @@ struct ConnectionConfigData {
|
||||
#[serde(default)]
|
||||
pub redis_database_aliases: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub redis_key_templates: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub etcd_endpoints: String,
|
||||
#[serde(default)]
|
||||
pub gbase_server: String,
|
||||
@@ -648,6 +654,7 @@ impl From<ConnectionConfigData> for ConnectionConfig {
|
||||
redis_key_separator: data.redis_key_separator,
|
||||
redis_scan_page_size: data.redis_scan_page_size,
|
||||
redis_database_aliases: data.redis_database_aliases,
|
||||
redis_key_templates: data.redis_key_templates,
|
||||
etcd_endpoints: data.etcd_endpoints,
|
||||
gbase_server: data.gbase_server,
|
||||
informix_server: data.informix_server,
|
||||
@@ -2631,6 +2638,7 @@ mod tests {
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -222,6 +222,7 @@ mod tests {
|
||||
redis_key_separator: String::new(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -917,6 +917,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -409,6 +409,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -676,6 +676,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
@@ -751,6 +752,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -686,6 +686,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: Some(1000),
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -5658,6 +5658,7 @@ for line in sys.stdin:
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
@@ -7648,6 +7649,7 @@ for line in sys.stdin:
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -3259,6 +3259,7 @@ mod tests {
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -4799,6 +4799,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
@@ -4866,6 +4867,7 @@ mod tests {
|
||||
redis_key_separator: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -11875,6 +11875,7 @@ SELECT 1 FROM dual"#
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -64,6 +64,7 @@ fn live_postgres_config(
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -60,6 +60,7 @@ fn live_postgres_config(
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -55,6 +55,7 @@ fn live_postgres_config(
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -60,6 +60,7 @@ fn live_postgres_config(
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -52,6 +52,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -61,6 +61,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -50,6 +50,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -174,6 +174,7 @@ pub fn postgres_test_config(id: &str, port: u16) -> ConnectionConfig {
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -813,6 +813,7 @@ mod tests {
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -594,6 +594,7 @@ mod tests {
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
@@ -269,6 +269,7 @@ mod tests {
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
redis_database_aliases: Default::default(),
|
||||
redis_key_templates: Vec::new(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
|
||||
Reference in New Issue
Block a user