diff --git a/bun.lockb b/bun.lockb index 484185f..b4c7e43 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d70d36a..ca53754 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@tabler/icons-react": "^3.19.0", "@tanstack/react-query": "^5.32.0", "@types/bytes": "^3.1.4", - "@upstash/redis": "^1.34.3", + "@upstash/redis": "^1.34.8", "bytes": "^3.1.2", "react-hook-form": "^7.53.0", "react-resizable-panels": "^2.1.4", diff --git a/src/components/databrowser/components/display/display-header.tsx b/src/components/databrowser/components/display/display-header.tsx index 3248cde..fc7f20b 100644 --- a/src/components/databrowser/components/display/display-header.tsx +++ b/src/components/databrowser/components/display/display-header.tsx @@ -5,7 +5,7 @@ import { IconPlus } from "@tabler/icons-react" import { Button } from "@/components/ui/button" import { TypeTag } from "../type-tag" -import { LengthBadge, SizeBadge, TTLBadge } from "./header-badges" +import { HeaderTTLBadge, LengthBadge, SizeBadge } from "./header-badges" import { KeyActions } from "./key-actions" export const DisplayHeader = ({ @@ -49,7 +49,7 @@ export const DisplayHeader = ({ - + ) diff --git a/src/components/databrowser/components/display/display-list-edit.tsx b/src/components/databrowser/components/display/display-list-edit.tsx index 8fd9016..d9fc046 100644 --- a/src/components/databrowser/components/display/display-list-edit.tsx +++ b/src/components/databrowser/components/display/display-list-edit.tsx @@ -3,6 +3,7 @@ import { useDatabrowserStore } from "@/store" import type { ListDataType } from "@/types" import { Controller, FormProvider, useForm, useFormContext } from "react-hook-form" +import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Spinner } from "@/components/ui/spinner" import { SimpleTooltip } from "@/components/ui/tooltip" @@ -10,6 +11,7 @@ import { SimpleTooltip } from "@/components/ui/tooltip" import { useFetchListItems } from "../../hooks" import { useEditListItem } from "../../hooks/use-edit-list-item" import { headerLabels } from "./display-list" +import { HashFieldTTLBadge } from "./hash/hash-field-ttl-badge" import { useField } from "./input/use-field" export const ListEditDisplay = ({ @@ -105,30 +107,43 @@ const ListEditForm = ({ )} -
- - +
+ {type === "hash" && itemKey !== "" && ( + + )} + +
- + + + +
diff --git a/src/components/databrowser/components/display/display-list.tsx b/src/components/databrowser/components/display/display-list.tsx index 2f9109e..39071fc 100644 --- a/src/components/databrowser/components/display/display-list.tsx +++ b/src/components/databrowser/components/display/display-list.tsx @@ -14,6 +14,7 @@ import { InfiniteScroll } from "../sidebar/infinite-scroll" import { DeleteAlertDialog } from "./delete-alert-dialog" import { DisplayHeader } from "./display-header" import { ListEditDisplay } from "./display-list-edit" +import { HashFieldTTLInfo } from "./hash/hash-field-ttl-info" export const headerLabels = { list: ["Index", "Content"], @@ -72,6 +73,7 @@ export const ListItems = ({ }) => { const { setSelectedListItem } = useDatabrowserStore() const keys = useMemo(() => query.data?.pages.flatMap((page) => page.keys) ?? [], [query.data]) + const fields = useMemo(() => keys.map((key) => key.key), [keys]) const { mutate: editItem } = useEditListItem() return ( @@ -84,7 +86,7 @@ export const ListItems = ({ onClick={() => { setSelectedListItem({ key }) }} - className="h-10 border-b border-b-zinc-100 hover:bg-zinc-50" + className={cn("h-10 border-b border-b-zinc-100 hover:bg-zinc-100 ")} > { e.stopPropagation() }} > - { - e.stopPropagation() - editItem({ - type, - dataKey, - itemKey: key, - // For deletion - newKey: undefined, - }) - }} - > - - +
+ {type === "hash" && ( + + )} + { + e.stopPropagation() + editItem({ + type, + dataKey, + itemKey: key, + // For deletion + newKey: undefined, + }) + }} + > + + +
)} diff --git a/src/components/databrowser/components/display/hash/hash-field-ttl-badge.tsx b/src/components/databrowser/components/display/hash/hash-field-ttl-badge.tsx new file mode 100644 index 0000000..99861fb --- /dev/null +++ b/src/components/databrowser/components/display/hash/hash-field-ttl-badge.tsx @@ -0,0 +1,20 @@ +import { useFetchHashFieldExpires } from "@/components/databrowser/hooks/use-fetch-hash-ttl" +import { useSetHashTTL } from "@/components/databrowser/hooks/use-set-hash-ttl" + +import { TTLBadge } from "../ttl-badge" + +export const HashFieldTTLBadge = ({ dataKey, field }: { dataKey: string; field: string }) => { + const { data } = useFetchHashFieldExpires({ dataKey, fields: [field] }) + const { mutate: setTTL, isPending } = useSetHashTTL() + + const expireAt = data?.[field] + + return ( + setTTL({ dataKey, field, ttl })} + isPending={isPending} + /> + ) +} diff --git a/src/components/databrowser/components/display/hash/hash-field-ttl-info.tsx b/src/components/databrowser/components/display/hash/hash-field-ttl-info.tsx new file mode 100644 index 0000000..2baa14e --- /dev/null +++ b/src/components/databrowser/components/display/hash/hash-field-ttl-info.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react" + +import { formatTime } from "@/lib/utils" +import { useFetchHashFieldExpires } from "@/components/databrowser/hooks/use-fetch-hash-ttl" + +import { calculateTTL, TTL_INFINITE, TTL_NOT_FOUND } from "../ttl-badge" + +export const HashFieldTTLInfo = ({ + dataKey, + field, + fields, +}: { + dataKey: string + field: string + fields: string[] +}) => { + const { data } = useFetchHashFieldExpires({ dataKey, fields }) + const expireAt = data?.[field] + + const [ttl, setTTL] = useState(() => calculateTTL(expireAt)) + + useEffect(() => { + setTTL(calculateTTL(expireAt)) + const interval = setInterval(() => { + setTTL(calculateTTL(expireAt)) + }, 1000) + + return () => clearInterval(interval) + }, [expireAt]) + + if (!expireAt || expireAt === TTL_NOT_FOUND || expireAt === TTL_INFINITE) return + + return ( + + {formatTime(ttl ?? 0)} + + ) +} diff --git a/src/components/databrowser/components/display/header-badges.tsx b/src/components/databrowser/components/display/header-badges.tsx index f1de5ce..6d304e6 100644 --- a/src/components/databrowser/components/display/header-badges.tsx +++ b/src/components/databrowser/components/display/header-badges.tsx @@ -1,17 +1,12 @@ -import { useEffect } from "react" import { type DataType } from "@/types" -import { IconChevronDown } from "@tabler/icons-react" import bytes from "bytes" -import { queryClient } from "@/lib/clients" -import { formatTime } from "@/lib/utils" import { Skeleton } from "@/components/ui/skeleton" -import { FETCH_TTL_QUERY_KEY, useFetchTTL } from "../../hooks" -import { useDeleteKeyCache } from "../../hooks/use-delete-key-cache" +import { useFetchKeyExpire, useSetTTL } from "../../hooks" import { useFetchKeyLength } from "../../hooks/use-fetch-key-length" import { useFetchKeySize } from "../../hooks/use-fetch-key-size" -import { TTLPopover } from "./ttl-popover" +import { TTLBadge } from "./ttl-badge" export const LengthBadge = ({ dataKey, @@ -50,43 +45,16 @@ export const SizeBadge = ({ dataKey }: { dataKey: string }) => { ) } -const TTL_INFINITE = -1 -const TTL_NOT_FOUND = -2 - -export const TTLBadge = ({ dataKey }: { dataKey: string }) => { - const { data: ttl } = useFetchTTL(dataKey) - const { deleteKeyCache } = useDeleteKeyCache() - - // Tick the ttl query every second - useEffect(() => { - const interval = setInterval(() => { - queryClient.setQueryData([FETCH_TTL_QUERY_KEY, dataKey], (ttl?: number) => { - if (ttl === undefined || ttl === TTL_INFINITE) return ttl - - if (ttl <= 1) { - deleteKeyCache(dataKey) - return TTL_NOT_FOUND - } - return ttl - 1 - }) - }, 1000) - - return () => clearInterval(interval) - }, []) +export const HeaderTTLBadge = ({ dataKey }: { dataKey: string }) => { + const { data: expireAt } = useFetchKeyExpire(dataKey) + const { mutate: setTTL, isPending } = useSetTTL() return ( - - {ttl === undefined ? ( - - ) : ( - -
- {ttl === TTL_INFINITE ? "Forever" : formatTime(ttl)} - -
-
- )} -
+ setTTL({ dataKey, ttl })} + isPending={isPending} + /> ) } diff --git a/src/components/databrowser/components/display/ttl-badge.tsx b/src/components/databrowser/components/display/ttl-badge.tsx new file mode 100644 index 0000000..d840ced --- /dev/null +++ b/src/components/databrowser/components/display/ttl-badge.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react" +import { IconChevronDown } from "@tabler/icons-react" + +import { formatTime } from "@/lib/utils" +import { Skeleton } from "@/components/ui/skeleton" + +import { Badge } from "./header-badges" +import { TTLPopover } from "./ttl-popover" + +export const TTL_INFINITE = -1 +export const TTL_NOT_FOUND = -2 + +export const calculateTTL = (expireAt?: number) => { + if (!expireAt) return + if (expireAt === TTL_INFINITE) return TTL_INFINITE + return Math.max(0, Math.floor((expireAt - Date.now()) / 1000)) +} + +export const TTLBadge = ({ + label = "TTL:", + expireAt, + setTTL, + isPending, +}: { + label?: string + expireAt?: number + setTTL: (ttl: number) => void + isPending: boolean +}) => { + const [ttl, setTTLLabel] = useState(() => calculateTTL(expireAt)) + + // Update ttl every second + useEffect(() => { + setTTLLabel(calculateTTL(expireAt)) + const interval = setInterval(() => { + setTTLLabel(calculateTTL(expireAt)) + }, 1000) + + return () => clearInterval(interval) + }, [expireAt]) + + return ( + + {ttl === undefined ? ( + + ) : ( + +
+ {ttl === TTL_INFINITE ? "Forever" : formatTime(ttl)} + +
+
+ )} +
+ ) +} diff --git a/src/components/databrowser/components/display/ttl-popover.tsx b/src/components/databrowser/components/display/ttl-popover.tsx index 47d23e6..c17bf2d 100644 --- a/src/components/databrowser/components/display/ttl-popover.tsx +++ b/src/components/databrowser/components/display/ttl-popover.tsx @@ -12,9 +12,8 @@ import { SelectValue, } from "@/components/ui/select" import { Spinner } from "@/components/ui/spinner" -import { useSetTTL } from "@/components/databrowser/hooks/use-set-ttl" -const PERSISTED_KEY = -1 +import { TTL_INFINITE } from "./ttl-badge" const timeUnits = [ { label: "Seconds", value: 1 }, @@ -26,10 +25,10 @@ const timeUnits = [ export function TTLPopover({ children, ttl, - dataKey, -}: PropsWithChildren<{ ttl: number; dataKey: string }>) { + setTTL, + isPending, +}: PropsWithChildren<{ ttl: number; setTTL: (ttl: number) => void; isPending: boolean }>) { const [open, setOpen] = useState(false) - const { mutateAsync: setTTL, isPending } = useSetTTL() const defaultValues = useMemo(() => { return { type: "Seconds", value: ttl } as const @@ -49,18 +48,12 @@ export function TTLPopover({ }, [defaultValues]) const onSubmit = handleSubmit(async ({ value, type }) => { - await setTTL({ - dataKey: dataKey, - ttl: value * timeUnits.find((unit) => unit.label === type)!.value, - }) + await setTTL(value * timeUnits.find((unit) => unit.label === type)!.value) setOpen(false) }) const handlePersist = async () => { - await setTTL({ - dataKey: dataKey, - ttl: undefined, - }) + await setTTL(TTL_INFINITE) setOpen(false) } @@ -77,7 +70,13 @@ export function TTLPopover({ -
+ { + onSubmit(e) + e.stopPropagation() + }} + >

Expiration

@@ -128,7 +127,7 @@ export function TTLPopover({