feat(redis): optimize TTL display with human-readable format (#1689)
This commit is contained in:
parent
c411cc6707
commit
941dfd5f82
|
|
@ -17,6 +17,7 @@ import { useTheme } from "@/composables/useTheme";
|
|||
import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle";
|
||||
import { createRedisShikiJsonHighlighter, type RedisJsonHighlighter } from "@/lib/redisJsonHighlighter";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import { formatTtl } from "@/lib/ttlFormat";
|
||||
import { canEditRedisMemberDetail, clampRedisMemberDetailSheetWidth, formatRedisMemberDetail, getRedisMemberSelectionKey } from "@/lib/redisValuePresentation";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -783,7 +784,7 @@ onBeforeUnmount(() => {
|
|||
<Badge variant="secondary" class="dbx-editor-font-family text-xs uppercase">{{ data.key_type }}</Badge>
|
||||
<Badge v-if="metadataSizeLabel" variant="outline" class="text-xs text-muted-foreground"> {{ t("redis.columnSize") }}: {{ metadataSizeLabel }} </Badge>
|
||||
<template v-if="!editingTtl">
|
||||
<Badge v-if="data.ttl > 0" variant="outline" class="text-xs cursor-pointer text-muted-foreground hover:bg-accent" @click="startEditTtl">TTL: {{ data.ttl }}s</Badge>
|
||||
<Badge v-if="data.ttl > 0" variant="outline" class="text-xs cursor-pointer text-muted-foreground hover:bg-accent" @click="startEditTtl">TTL: {{ formatTtl(data.ttl, t) }}</Badge>
|
||||
<Badge v-else-if="data.ttl === -1" variant="outline" class="text-xs cursor-pointer text-muted-foreground hover:bg-accent" @click="startEditTtl">{{ t("redis.noExpiry") }}</Badge>
|
||||
</template>
|
||||
<div v-else class="flex items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -1574,6 +1574,7 @@ export default {
|
|||
loadedMembers: "{loaded} of {total} members loaded",
|
||||
entries: "{count} entries",
|
||||
noExpiry: "no expiry",
|
||||
ttlDay: "{count}d",
|
||||
columnType: "Type",
|
||||
columnKey: "Key",
|
||||
columnValue: "Value",
|
||||
|
|
|
|||
|
|
@ -1308,6 +1308,7 @@ export default {
|
|||
loadedMembers: "{loaded} de {total} miembros cargados",
|
||||
entries: "{count} entradas",
|
||||
noExpiry: "sin expiración",
|
||||
ttlDay: "{count}d",
|
||||
columnType: "Tipo",
|
||||
columnKey: "Clave",
|
||||
columnValue: "Valor",
|
||||
|
|
|
|||
|
|
@ -1432,6 +1432,7 @@ export default {
|
|||
loadedMembers: "{loaded} di {total} membri caricati",
|
||||
entries: "{count} voci",
|
||||
noExpiry: "nessuna scadenza",
|
||||
ttlDay: "{count}g",
|
||||
columnType: "Tipo",
|
||||
columnKey: "Chiave",
|
||||
columnValue: "Valore",
|
||||
|
|
|
|||
|
|
@ -1561,6 +1561,7 @@ export default {
|
|||
loadedMembers: "{loaded}/{total}メンバー読み込み完了",
|
||||
entries: "{count}エントリ",
|
||||
noExpiry: "期限なし",
|
||||
ttlDay: "{count}日",
|
||||
columnType: "型",
|
||||
columnKey: "キー",
|
||||
columnValue: "値",
|
||||
|
|
|
|||
|
|
@ -1443,6 +1443,7 @@ export default {
|
|||
loadedMembers: "{loaded} de {total} membros carregados",
|
||||
entries: "{count} entradas",
|
||||
noExpiry: "sem expiração",
|
||||
ttlDay: "{count}d",
|
||||
columnType: "Tipo",
|
||||
columnKey: "Chave",
|
||||
columnValue: "Valor",
|
||||
|
|
|
|||
|
|
@ -1573,6 +1573,7 @@ export default {
|
|||
loadedMembers: "已加载 {loaded} / 共 {total} 个成员",
|
||||
entries: "{count} 条记录",
|
||||
noExpiry: "永不过期",
|
||||
ttlDay: "{count}天",
|
||||
columnType: "类型",
|
||||
columnKey: "键",
|
||||
columnValue: "值",
|
||||
|
|
|
|||
|
|
@ -1423,6 +1423,7 @@ export default {
|
|||
loadedMembers: "已載入 {loaded} / 共 {total} 個成員",
|
||||
entries: "{count} 筆項目",
|
||||
noExpiry: "永不過期",
|
||||
ttlDay: "{count}天",
|
||||
columnType: "類型",
|
||||
columnKey: "鍵",
|
||||
columnValue: "值",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { formatTtl } from "@/lib/ttlFormat";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock `t()` helpers — only the day unit needs localization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function enT(key: string, options?: Record<string, unknown>): string {
|
||||
const messages: Record<string, string> = {
|
||||
"redis.ttlDay": "{count}d",
|
||||
};
|
||||
const msg = messages[key] ?? key;
|
||||
const count = (options as { count?: number })?.count;
|
||||
if (count != null) return msg.replace("{count}", String(count));
|
||||
return msg;
|
||||
}
|
||||
|
||||
function zhT(key: string, options?: Record<string, unknown>): string {
|
||||
const messages: Record<string, string> = {
|
||||
"redis.ttlDay": "{count}天",
|
||||
};
|
||||
const msg = messages[key] ?? key;
|
||||
const count = (options as { count?: number })?.count;
|
||||
if (count != null) return msg.replace("{count}", String(count));
|
||||
return msg;
|
||||
}
|
||||
|
||||
function esT(key: string, options?: Record<string, unknown>): string {
|
||||
const messages: Record<string, string> = {
|
||||
"redis.ttlDay": "{count}d",
|
||||
};
|
||||
const msg = messages[key] ?? key;
|
||||
const count = (options as { count?: number })?.count;
|
||||
if (count != null) return msg.replace("{count}", String(count));
|
||||
return msg;
|
||||
}
|
||||
|
||||
function itT(key: string, options?: Record<string, unknown>): string {
|
||||
const messages: Record<string, string> = {
|
||||
"redis.ttlDay": "{count}g",
|
||||
};
|
||||
const msg = messages[key] ?? key;
|
||||
const count = (options as { count?: number })?.count;
|
||||
if (count != null) return msg.replace("{count}", String(count));
|
||||
return msg;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("formatTtl — special values", () => {
|
||||
it("returns null for -1 (no expiry)", () => {
|
||||
expect(formatTtl(-1, enT)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for 0", () => {
|
||||
expect(formatTtl(0, enT)).toBeNull();
|
||||
expect(formatTtl(0, zhT)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for negative values other than -1", () => {
|
||||
expect(formatTtl(-2, enT)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTtl — sub-day, HH:mm:ss (no i18n)", () => {
|
||||
it("formats 45s as 00:00:45", () => {
|
||||
expect(formatTtl(45, enT)).toBe("00:00:45");
|
||||
});
|
||||
|
||||
it("formats 1m as 00:01:00", () => {
|
||||
expect(formatTtl(60, enT)).toBe("00:01:00");
|
||||
});
|
||||
|
||||
it("formats 1m 30s as 00:01:30", () => {
|
||||
expect(formatTtl(90, enT)).toBe("00:01:30");
|
||||
});
|
||||
|
||||
it("formats 2m 5s as 00:02:05", () => {
|
||||
expect(formatTtl(125, enT)).toBe("00:02:05");
|
||||
});
|
||||
|
||||
it("formats 1h as 01:00:00", () => {
|
||||
expect(formatTtl(3600, enT)).toBe("01:00:00");
|
||||
});
|
||||
|
||||
it("formats 1h 1m 1s as 01:01:01", () => {
|
||||
expect(formatTtl(3661, enT)).toBe("01:01:01");
|
||||
});
|
||||
|
||||
it("formats 23h 59m 59s as 23:59:59", () => {
|
||||
expect(formatTtl(86399, enT)).toBe("23:59:59");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTtl — multi-day, {n}d HH:mm:ss (day unit localized)", () => {
|
||||
it("formats exactly 1 day as 1d 00:00:00", () => {
|
||||
expect(formatTtl(86400, enT)).toBe("1d 00:00:00");
|
||||
expect(formatTtl(86400, zhT)).toBe("1天 00:00:00");
|
||||
});
|
||||
|
||||
it("formats 7 days as 7d 00:00:00", () => {
|
||||
expect(formatTtl(604800, enT)).toBe("7d 00:00:00");
|
||||
expect(formatTtl(604800, zhT)).toBe("7天 00:00:00");
|
||||
});
|
||||
|
||||
it("formats 365 days as 365d 00:00:00", () => {
|
||||
expect(formatTtl(31536000, enT)).toBe("365d 00:00:00");
|
||||
expect(formatTtl(31536000, zhT)).toBe("365天 00:00:00");
|
||||
});
|
||||
|
||||
it("formats 1d 1h 1m 1s", () => {
|
||||
expect(formatTtl(90061, enT)).toBe("1d 01:01:01");
|
||||
expect(formatTtl(90061, zhT)).toBe("1天 01:01:01");
|
||||
});
|
||||
|
||||
it("formats 2d 3h 4m 5s", () => {
|
||||
const ttl = 2 * 86400 + 3 * 3600 + 4 * 60 + 5; // 183845
|
||||
expect(formatTtl(ttl, enT)).toBe("2d 03:04:05");
|
||||
expect(formatTtl(ttl, zhT)).toBe("2天 03:04:05");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTtl — Spanish locale", () => {
|
||||
it("formats sub-day values with no i18n", () => {
|
||||
expect(formatTtl(45, esT)).toBe("00:00:45");
|
||||
expect(formatTtl(3661, esT)).toBe("01:01:01");
|
||||
});
|
||||
|
||||
it("formats multi-day with localized d", () => {
|
||||
expect(formatTtl(86400, esT)).toBe("1d 00:00:00");
|
||||
expect(formatTtl(604800, esT)).toBe("7d 00:00:00");
|
||||
});
|
||||
|
||||
it("returns null for -1", () => {
|
||||
expect(formatTtl(-1, esT)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for 0", () => {
|
||||
expect(formatTtl(0, esT)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTtl — Italian locale", () => {
|
||||
it("formats sub-day values with no i18n", () => {
|
||||
expect(formatTtl(45, itT)).toBe("00:00:45");
|
||||
expect(formatTtl(3661, itT)).toBe("01:01:01");
|
||||
});
|
||||
|
||||
it("formats multi-day with localized g", () => {
|
||||
expect(formatTtl(86400, itT)).toBe("1g 00:00:00");
|
||||
expect(formatTtl(604800, itT)).toBe("7g 00:00:00");
|
||||
});
|
||||
|
||||
it("returns null for -1", () => {
|
||||
expect(formatTtl(-1, itT)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for 0", () => {
|
||||
expect(formatTtl(0, itT)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Formats Redis TTL (seconds) as a compact string:
|
||||
* < 1 day → HH:mm:ss
|
||||
* >= 1 day → {n}d HH:mm:ss (day unit localized)
|
||||
*
|
||||
* Returns null for ttl === -1 (no expiry) or ttl <= 0.
|
||||
*/
|
||||
|
||||
type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
|
||||
|
||||
export function formatTtl(ttl: number, t: TranslateFn): string | null {
|
||||
if (ttl === -1) return null;
|
||||
if (ttl <= 0) return null;
|
||||
|
||||
const days = Math.floor(ttl / 86_400);
|
||||
const hours = Math.floor((ttl % 86_400) / 3_600);
|
||||
const minutes = Math.floor((ttl % 3_600) / 60);
|
||||
const seconds = ttl % 60;
|
||||
|
||||
const time = [String(hours).padStart(2, "0"), String(minutes).padStart(2, "0"), String(seconds).padStart(2, "0")].join(":");
|
||||
|
||||
if (days > 0) {
|
||||
return `${t("redis.ttlDay", { count: days })} ${time}`;
|
||||
}
|
||||
return time;
|
||||
}
|
||||
Loading…
Reference in New Issue