feat(redis): add all search mode
This commit is contained in:
parent
ca3e3a2a56
commit
a4bd62005b
|
|
@ -35,7 +35,7 @@ const connectionStore = useConnectionStore();
|
|||
const settingsStore = useSettingsStore();
|
||||
const editorFontFamilyStyle = useEditorFontFamilyStyle();
|
||||
|
||||
type RedisSearchMode = "key" | "value";
|
||||
type RedisSearchMode = "key" | "value" | "all";
|
||||
type RedisCreateKeyType = "string" | "hash" | "list" | "set" | "zset" | "stream" | "json";
|
||||
|
||||
interface CreateKeyEntry {
|
||||
|
|
@ -100,10 +100,14 @@ let redisBrowserIsActive = true;
|
|||
let redisDbFlushedListenerRegistered = false;
|
||||
|
||||
const valueQuery = computed(() => searchPattern.value.trim());
|
||||
const isValueSearchMode = computed(() => searchMode.value === "value" || searchMode.value === "all");
|
||||
const effectivePattern = computed(() => (searchMode.value === "key" ? redisKeySearchPattern(searchPattern.value, fuzzyKeySearch.value) : "*"));
|
||||
const isSearchMode = computed(() => (searchMode.value === "key" ? effectivePattern.value !== "*" : valueQuery.value !== ""));
|
||||
const searchPlaceholder = computed(() => (searchMode.value === "key" ? (fuzzyKeySearch.value ? t("redis.fuzzyPattern") : t("redis.pattern")) : t("redis.valueSearchPlaceholder")));
|
||||
const loadingEmptyText = computed(() => (searchMode.value === "value" && valueQuery.value ? t("redis.searchingValues") : t("redis.loadingKeys")));
|
||||
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 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 ?? ":");
|
||||
watch(redisKeySeparator, () => {
|
||||
if (flatKeys.value.length > 0) rebuildTree(false);
|
||||
|
|
@ -192,7 +196,7 @@ function mergeTree(newKeys: RedisKeyInfo[]) {
|
|||
|
||||
async function fetchScanPage(): Promise<RedisScanResult> {
|
||||
const pageSize = settingsStore.editorSettings.redisScanPageSize;
|
||||
return searchMode.value === "value" ? await api.redisScanValues(props.connectionId, props.db, scanCursor.value, "*", valueQuery.value, pageSize) : await api.redisScanKeys(props.connectionId, props.db, scanCursor.value, effectivePattern.value, pageSize);
|
||||
return isValueSearchMode.value ? await api.redisScanValues(props.connectionId, props.db, scanCursor.value, "*", valueQuery.value, pageSize, searchMode.value === "all") : await api.redisScanKeys(props.connectionId, props.db, scanCursor.value, effectivePattern.value, pageSize);
|
||||
}
|
||||
|
||||
function appendScanResult(result: RedisScanResult) {
|
||||
|
|
@ -223,7 +227,7 @@ async function scanNextPage(requestId = searchRequestId): Promise<boolean> {
|
|||
}
|
||||
|
||||
async function streamValueSearch(requestId: number) {
|
||||
while (requestId === searchRequestId && searchMode.value === "value" && valueQuery.value && hasMore.value) {
|
||||
while (requestId === searchRequestId && isValueSearchMode.value && valueQuery.value && hasMore.value) {
|
||||
const applied = await scanNextPage(requestId);
|
||||
if (!applied) return;
|
||||
}
|
||||
|
|
@ -255,13 +259,13 @@ async function loadKeys() {
|
|||
expandedGroupIds.value = new Set();
|
||||
scanCursor.value = 0;
|
||||
try {
|
||||
if (searchMode.value === "value" && !valueQuery.value) {
|
||||
if (isValueSearchMode.value && !valueQuery.value) {
|
||||
hasMore.value = false;
|
||||
return;
|
||||
}
|
||||
const applied = await scanNextPage(requestId);
|
||||
if (applied) {
|
||||
if (searchMode.value === "value") {
|
||||
if (isValueSearchMode.value) {
|
||||
await streamValueSearch(requestId);
|
||||
} else {
|
||||
await fillInitialKeyBatch(requestId);
|
||||
|
|
@ -849,6 +853,9 @@ defineExpose({ focusSearch });
|
|||
<button type="button" class="h-5 px-2 text-xs rounded-sm transition-colors" :class="searchMode === 'value' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'" @click="setSearchMode('value')">
|
||||
{{ t("redis.searchByValue") }}
|
||||
</button>
|
||||
<button type="button" class="h-5 px-2 text-xs rounded-sm transition-colors" :class="searchMode === 'all' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'" @click="setSearchMode('all')">
|
||||
{{ t("redis.searchByAll") }}
|
||||
</button>
|
||||
</div>
|
||||
<Input v-model="searchPattern" data-redis-search-input class="h-6 text-xs border-0 shadow-none focus-visible:ring-0" :placeholder="searchPlaceholder" @input="onSearchInput" @keydown="onSearchKeydown" />
|
||||
<Button v-if="searchMode === 'key'" variant="ghost" size="sm" class="h-6 shrink-0 px-2 text-xs" :class="fuzzyKeySearch ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'" :title="t('redis.fuzzyMatchTitle')" :aria-pressed="fuzzyKeySearch" @click="toggleFuzzyKeySearch">
|
||||
|
|
|
|||
|
|
@ -1388,11 +1388,14 @@
|
|||
fuzzyMatch: "Fuzzy",
|
||||
fuzzyMatchTitle: "Fuzzy match: search keys by plain text contains",
|
||||
valueSearchPlaceholder: "value contains...",
|
||||
allSearchPlaceholder: "key or value contains...",
|
||||
searchByKey: "Key",
|
||||
searchByValue: "Value",
|
||||
searchByAll: "All",
|
||||
keys: "{count} keys",
|
||||
loadingKeys: "Loading keys...",
|
||||
searchingValues: "Searching values...",
|
||||
searchingAll: "Searching keys and values...",
|
||||
loadMoreKeys: "Load more keys",
|
||||
fetchAllKeys: "Fetch all",
|
||||
stopFetchAll: "Stop",
|
||||
|
|
|
|||
|
|
@ -1171,11 +1171,14 @@
|
|||
fuzzyMatch: "Difusa",
|
||||
fuzzyMatchTitle: "Coincidencia difusa: busca claves que contengan texto plano",
|
||||
valueSearchPlaceholder: "el valor contiene...",
|
||||
allSearchPlaceholder: "la clave o el valor contiene...",
|
||||
searchByKey: "Clave",
|
||||
searchByValue: "Valor",
|
||||
searchByAll: "Todo",
|
||||
keys: "{count} claves",
|
||||
loadingKeys: "Cargando claves...",
|
||||
searchingValues: "Buscando por valor...",
|
||||
searchingAll: "Buscando claves y valores...",
|
||||
loadMoreKeys: "Cargar más claves",
|
||||
fetchAllKeys: "Cargar todas",
|
||||
stopFetchAll: "Detener",
|
||||
|
|
|
|||
|
|
@ -1281,11 +1281,14 @@
|
|||
fuzzyMatch: "Fuzzy",
|
||||
fuzzyMatchTitle: "Corrispondenza fuzzy: cerca le chiavi per testo semplice contenuto",
|
||||
valueSearchPlaceholder: "il valore contiene...",
|
||||
allSearchPlaceholder: "la chiave o il valore contiene...",
|
||||
searchByKey: "Chiave",
|
||||
searchByValue: "Valore",
|
||||
searchByAll: "Tutto",
|
||||
keys: "{count} chiavi",
|
||||
loadingKeys: "Caricamento chiavi...",
|
||||
searchingValues: "Ricerca nei valori...",
|
||||
searchingAll: "Ricerca in chiavi e valori...",
|
||||
loadMoreKeys: "Carica altre chiavi",
|
||||
fetchAllKeys: "Recupera tutto",
|
||||
stopFetchAll: "Interrompi",
|
||||
|
|
|
|||
|
|
@ -1281,11 +1281,14 @@
|
|||
fuzzyMatch: "Aproximado",
|
||||
fuzzyMatchTitle: "Correspondência aproximada: pesquisa chaves por texto contido",
|
||||
valueSearchPlaceholder: "o valor contém...",
|
||||
allSearchPlaceholder: "a chave ou o valor contém...",
|
||||
searchByKey: "Chave",
|
||||
searchByValue: "Valor",
|
||||
searchByAll: "Tudo",
|
||||
keys: "{count} chaves",
|
||||
loadingKeys: "Carregando chaves...",
|
||||
searchingValues: "Pesquisando valores...",
|
||||
searchingAll: "Pesquisando chaves e valores...",
|
||||
loadMoreKeys: "Carregar mais chaves",
|
||||
fetchAllKeys: "Buscar todas",
|
||||
stopFetchAll: "Parar",
|
||||
|
|
|
|||
|
|
@ -1387,11 +1387,14 @@
|
|||
fuzzyMatch: "模糊",
|
||||
fuzzyMatchTitle: "模糊匹配:自动按包含关系搜索 key",
|
||||
valueSearchPlaceholder: "按值内容搜索...",
|
||||
allSearchPlaceholder: "按 key 或值搜索...",
|
||||
searchByKey: "键",
|
||||
searchByValue: "值",
|
||||
searchByAll: "全部",
|
||||
keys: "{count} 个 key",
|
||||
loadingKeys: "正在加载 key...",
|
||||
searchingValues: "正在按值搜索...",
|
||||
searchingAll: "正在搜索 key 和值...",
|
||||
loadMoreKeys: "加载更多",
|
||||
fetchAllKeys: "获取全部",
|
||||
stopFetchAll: "停止",
|
||||
|
|
|
|||
|
|
@ -1260,11 +1260,14 @@
|
|||
fuzzyMatch: "模糊",
|
||||
fuzzyMatchTitle: "模糊匹配:自動按包含關係搜尋 key",
|
||||
valueSearchPlaceholder: "值包含……",
|
||||
allSearchPlaceholder: "鍵或值包含……",
|
||||
searchByKey: "鍵",
|
||||
searchByValue: "值",
|
||||
searchByAll: "全部",
|
||||
keys: "{count} 個 key",
|
||||
loadingKeys: "正在載入 key……",
|
||||
searchingValues: "正在按值搜尋……",
|
||||
searchingAll: "正在搜尋鍵和值……",
|
||||
loadMoreKeys: "載入更多",
|
||||
fetchAllKeys: "取得全部",
|
||||
stopFetchAll: "停止",
|
||||
|
|
|
|||
|
|
@ -1148,8 +1148,8 @@ export async function redisScanKeys(connectionId: string, db: number, cursor: nu
|
|||
return post("/api/redis/scan-keys", { connectionId, db, cursor, pattern, count });
|
||||
}
|
||||
|
||||
export async function redisScanValues(connectionId: string, db: number, cursor: number, pattern: string, query: string, count: number): Promise<RedisScanResult> {
|
||||
return post("/api/redis/scan-values", { connectionId, db, cursor, pattern, query, count });
|
||||
export async function redisScanValues(connectionId: string, db: number, cursor: number, pattern: string, query: string, count: number, includeKeyMatches = false): Promise<RedisScanResult> {
|
||||
return post("/api/redis/scan-values", { connectionId, db, cursor, pattern, query, includeKeyMatches, count });
|
||||
}
|
||||
|
||||
export async function redisGetValue(connectionId: string, db: number, keyRaw: string): Promise<RedisValue> {
|
||||
|
|
|
|||
|
|
@ -1007,8 +1007,8 @@ export async function redisScanKeys(connectionId: string, db: number, cursor: nu
|
|||
return invoke("redis_scan_keys", { connectionId, db, cursor, pattern, count });
|
||||
}
|
||||
|
||||
export async function redisScanValues(connectionId: string, db: number, cursor: number, pattern: string, query: string, count: number): Promise<RedisScanResult> {
|
||||
return invoke("redis_scan_values", { connectionId, db, cursor, pattern, query, count });
|
||||
export async function redisScanValues(connectionId: string, db: number, cursor: number, pattern: string, query: string, count: number, includeKeyMatches = false): Promise<RedisScanResult> {
|
||||
return invoke("redis_scan_values", { connectionId, db, cursor, pattern, query, includeKeyMatches, count });
|
||||
}
|
||||
|
||||
export async function redisGetValue(connectionId: string, db: number, keyRaw: string): Promise<RedisValue> {
|
||||
|
|
|
|||
|
|
@ -582,6 +582,7 @@ pub async fn scan_cluster_values_page(
|
|||
cursor: u64,
|
||||
pattern: &str,
|
||||
query: &str,
|
||||
include_key_matches: bool,
|
||||
count: usize,
|
||||
) -> Result<RedisScanResult, String> {
|
||||
let master_nodes = cluster_master_nodes(pool).await?;
|
||||
|
|
@ -600,7 +601,7 @@ pub async fn scan_cluster_values_page(
|
|||
let mut con =
|
||||
connect_direct_node(endpoint, pool.tls, pool.tls_insecure, &pool.username, &pool.password).await?;
|
||||
let current_cursor = if index == node_index { node_cursor } else { 0 };
|
||||
let result = scan_values_page(&mut con, current_cursor, pattern, query, count).await?;
|
||||
let result = scan_values_page(&mut con, current_cursor, pattern, query, include_key_matches, count).await?;
|
||||
if !result.keys.is_empty() {
|
||||
let next_cursor = if result.cursor != 0 {
|
||||
encode_cluster_cursor(index, result.cursor)?
|
||||
|
|
@ -962,6 +963,7 @@ pub async fn scan_values_page<C>(
|
|||
cursor: u64,
|
||||
pattern: &str,
|
||||
query: &str,
|
||||
include_key_matches: bool,
|
||||
count: usize,
|
||||
) -> Result<RedisScanResult, String>
|
||||
where
|
||||
|
|
@ -985,7 +987,47 @@ where
|
|||
|
||||
let (next_cursor, keys) = parse_scan_keys(raw)?;
|
||||
let mut result = Vec::new();
|
||||
for key in keys {
|
||||
let keys: Vec<_> = keys
|
||||
.into_iter()
|
||||
.map(|key| {
|
||||
let key_display = redis_key_bytes_to_display(&key);
|
||||
let key_raw = redis_key_bytes_to_raw(&key);
|
||||
let key_matches = include_key_matches && redis_key_matches_query(&key_display, &key_raw, query);
|
||||
(key, key_display, key_raw, key_matches)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut key_match_types = Vec::new();
|
||||
if include_key_matches {
|
||||
let mut pipe = redis::pipe();
|
||||
let mut key_match_count = 0usize;
|
||||
for (key, _, _, key_matches) in &keys {
|
||||
if *key_matches {
|
||||
pipe.cmd("TYPE").arg(key);
|
||||
key_match_count += 1;
|
||||
}
|
||||
}
|
||||
if key_match_count > 0 {
|
||||
key_match_types = pipe.query_async(con).await.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
let mut key_match_type_index = 0usize;
|
||||
for (key, key_display, key_raw, key_matches) in keys {
|
||||
if key_matches {
|
||||
let key_type = key_match_types.get(key_match_type_index).cloned().unwrap_or_else(|| "unknown".to_string());
|
||||
key_match_type_index += 1;
|
||||
result.push(RedisKeyInfo {
|
||||
key_display,
|
||||
key_raw,
|
||||
value_preview: redis_key_value_preview(&key_type),
|
||||
key_type,
|
||||
ttl: -2,
|
||||
size: 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(value) = get_value(con, &key).await else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -1076,6 +1118,15 @@ fn redis_value_matches_query(value: &serde_json::Value, query: &str) -> bool {
|
|||
redis_search_value_text(value).to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
fn redis_key_matches_query(key_display: &str, key_raw: &str, query: &str) -> bool {
|
||||
let query = query.trim();
|
||||
if query.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let query = query.to_lowercase();
|
||||
key_display.to_lowercase().contains(&query) || key_raw.to_lowercase().contains(&query)
|
||||
}
|
||||
|
||||
fn redis_search_value_text(value: &serde_json::Value) -> String {
|
||||
match value {
|
||||
serde_json::Value::String(text) => text.clone(),
|
||||
|
|
@ -1625,8 +1676,9 @@ mod tests {
|
|||
parse_cluster_slots, parse_command_argv, parse_database_count, parse_redis_endpoint, parse_scan_keys,
|
||||
parse_stream_entries, redis_auth_candidates, redis_command_raw_to_json, redis_database_index,
|
||||
redis_json_raw_to_json, redis_json_value_preview, redis_key_bytes_to_display, redis_key_bytes_to_raw,
|
||||
redis_key_raw_to_bytes, redis_key_value_preview, redis_raw_to_json, redis_value_contains_binary,
|
||||
redis_value_matches_query, RedisAuthCandidate, RedisCommandSafety, RedisNodeEndpoint, RedisRawValue,
|
||||
redis_key_matches_query, redis_key_raw_to_bytes, redis_key_value_preview, redis_raw_to_json,
|
||||
redis_value_contains_binary, redis_value_matches_query, RedisAuthCandidate, RedisCommandSafety,
|
||||
RedisNodeEndpoint, RedisRawValue,
|
||||
};
|
||||
use crate::models::connection::ConnectionConfig;
|
||||
use redis::ConnectionAddr;
|
||||
|
|
@ -1767,6 +1819,14 @@ mod tests {
|
|||
assert!(!redis_value_matches_query(&serde_json::json!("Hello Redis"), "mysql"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_redis_keys_case_insensitively() {
|
||||
assert!(redis_key_matches_query("User:42:Profile", "User:42:Profile", "profile"));
|
||||
assert!(redis_key_matches_query("binary key", "ff75736572", "FF75"));
|
||||
assert!(!redis_key_matches_query("User:42:Profile", "User:42:Profile", ""));
|
||||
assert!(!redis_key_matches_query("User:42:Profile", "User:42:Profile", "order"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_hash_field_name_in_value_search() {
|
||||
let hash_value = serde_json::json!([
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ pub async fn redis_scan_values_core(
|
|||
cursor: u64,
|
||||
pattern: &str,
|
||||
query: &str,
|
||||
include_key_matches: bool,
|
||||
count: usize,
|
||||
) -> Result<RedisScanResult, String> {
|
||||
let connections = state.connections.read().await;
|
||||
|
|
@ -63,11 +64,12 @@ pub async fn redis_scan_values_core(
|
|||
RedisConnection::Direct(con) => {
|
||||
let mut con = con.lock().await;
|
||||
redis_driver::select_db(&mut *con, db).await?;
|
||||
redis_driver::scan_values_page(&mut *con, cursor, pattern, query, count).await
|
||||
redis_driver::scan_values_page(&mut *con, cursor, pattern, query, include_key_matches, count).await
|
||||
}
|
||||
RedisConnection::Cluster(cluster) => {
|
||||
redis_driver::ensure_cluster_db(db)?;
|
||||
redis_driver::scan_cluster_values_page(cluster, cursor, pattern, query, count).await
|
||||
redis_driver::scan_cluster_values_page(cluster, cursor, pattern, query, include_key_matches, count)
|
||||
.await
|
||||
}
|
||||
},
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ pub struct RedisValueScanRequest {
|
|||
pub cursor: u64,
|
||||
pub pattern: String,
|
||||
pub query: String,
|
||||
pub include_key_matches: Option<bool>,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +193,7 @@ pub async fn scan_values(
|
|||
req.cursor,
|
||||
&req.pattern,
|
||||
&req.query,
|
||||
req.include_key_matches.unwrap_or(false),
|
||||
req.count,
|
||||
)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -34,9 +34,20 @@ pub async fn redis_scan_values(
|
|||
cursor: u64,
|
||||
pattern: String,
|
||||
query: String,
|
||||
include_key_matches: Option<bool>,
|
||||
count: usize,
|
||||
) -> Result<RedisScanResult, String> {
|
||||
dbx_core::redis_ops::redis_scan_values_core(&state, &connection_id, db, cursor, &pattern, &query, count).await
|
||||
dbx_core::redis_ops::redis_scan_values_core(
|
||||
&state,
|
||||
&connection_id,
|
||||
db,
|
||||
cursor,
|
||||
&pattern,
|
||||
&query,
|
||||
include_key_matches.unwrap_or(false),
|
||||
count,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
|
|||
Loading…
Reference in New Issue