From 4c98b87cec21e155ef896d25e4254cc4a778d3dc Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 10 Jun 2026 01:39:09 +0200 Subject: [PATCH] feat(keys): add version history plumbing --- src/app/os/keys/page.tsx | 366 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 354 insertions(+), 12 deletions(-) diff --git a/src/app/os/keys/page.tsx b/src/app/os/keys/page.tsx index 086f3e1..aced8ad 100644 --- a/src/app/os/keys/page.tsx +++ b/src/app/os/keys/page.tsx @@ -1,44 +1,386 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; -import { useSearchParams } from "next/navigation"; +import { + ChevronDown, + ChevronRight, + CircleMinus, + CirclePlus, + GitCompare, + History, + Minus, + Plus, + Search, + X, +} from "lucide-react"; import Link from "next/link"; -import { Search, X } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; import { HeaderPortal } from "@/components/header-portal"; -import { createEngine } from "@/lib/engine"; -import { tokenizeKeys, getTopTokens } from "@/lib/tokenizer"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { useQueryFilter } from "@/hooks/use-query-filter"; +import { createEngine } from "@/lib/engine"; +import { getTopTokens, tokenizeKeys } from "@/lib/tokenizer"; +import type { OS } from "@/lib/types"; +import { cn } from "@/lib/utils"; + +function versionTag(os: OS) { + return `${os.version}_${os.build}`; +} + +function isCurrentVersion(os: OS, build: string) { + return os.build === build || versionTag(os) === build; +} + +function matchesVersion(os: OS, tag: string) { + return os.build === tag || versionTag(os) === tag; +} + +function compareOSVersion(a: OS, b: OS) { + const vA = a.version.split(".").map(Number); + const vB = b.version.split(".").map(Number); + for (let i = 0; i < Math.max(vA.length, vB.length); i++) { + const diff = (vB[i] || 0) - (vA[i] || 0); + if (diff !== 0) return diff; + } + + return b.build.localeCompare(a.build); +} + +const keySkeletons = Array.from( + { length: 30 }, + (_, index) => `key-skeleton-${index}`, +); + +type DisplayedKey = { + key: string; + os: string; + status: "current" | "added" | "removed"; +}; + +type VersionHistoryPanelProps = { + activeCompareTag: string | null; + build: string; + className?: string; + compareWith: string | null; + group: string; + groupedVersions: Array<[string, OS[]]>; + listClassName?: string; + setCompareWith: (value: string | null) => void; + switchVersion: (version: OS) => void; + versionsCount: number; +}; + +function KeyLink({ entry }: { entry: DisplayedKey }) { + const isNew = entry.status === "added"; + const isRemoved = entry.status === "removed"; + + return ( + + {(isNew || isRemoved) && ( + + {isRemoved ? ( + + )} + {entry.key} + + ); +} + +function VersionHistoryPanel({ + activeCompareTag, + build, + className, + compareWith, + group, + groupedVersions, + listClassName, + setCompareWith, + switchVersion, + versionsCount, +}: VersionHistoryPanelProps) { + return ( +
+
+

+ Version History +

+ {versionsCount} +
+ +
+ {groupedVersions.map(([major, groupVersions]) => ( +
+ isCurrentVersion(version, build) || + (activeCompareTag && matchesVersion(version, activeCompareTag)), + )} + className="group" + > + + + + {group} {major}.x + + + {groupVersions.length} + + + +
+ {groupVersions.map((version) => { + const tag = versionTag(version); + const isCurrent = isCurrentVersion(version, build); + const isComparing = activeCompareTag + ? matchesVersion(version, activeCompareTag) + : false; + + return ( +
+ + + {!isCurrent && ( + + )} +
+ ); + })} +
+
+ ))} +
+
+ ); +} export default function Keys() { const params = useSearchParams(); + const router = useRouter(); const os = params.get("os") as string; const [group, build] = os ? os.split("/") : ["", ""]; const [loading, setLoading] = useState(true); const [keys, setKeys] = useState([]); + const [versions, setVersions] = useState([]); + const [compareKeys, setCompareKeys] = useState(null); + const [compareLoading, setCompareLoading] = useState(false); + const [compareError, setCompareError] = useState(false); + const [changesExpanded, setChangesExpanded] = useState(false); const { keyword, setKeyword, debouncedKeyword } = useQueryFilter(); + const compareWith = params.get("diff"); + useEffect(() => { + let cancelled = false; + async function load() { + if (!group || !build) { + setKeys([]); + setVersions([]); + return; + } + const engine = await createEngine(group); - const allKeys = await engine.getKeys(build); + const [allKeys, osList] = await Promise.all([ + engine.getKeys(build), + engine.listOS().catch(() => [] as OS[]), + ]); allKeys.sort((a, b) => a.localeCompare(b)); - setKeys(allKeys); + osList.sort(compareOSVersion); + + if (!cancelled) { + setKeys(allKeys); + setVersions(osList); + } } setLoading(true); - load().finally(() => setLoading(false)); + load() + .catch(() => { + if (!cancelled) { + setKeys([]); + setVersions([]); + } + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; }, [group, build]); + const currentVersionIndex = useMemo( + () => versions.findIndex((version) => isCurrentVersion(version, build)), + [build, versions], + ); + + const currentVersion = + currentVersionIndex === -1 ? undefined : versions[currentVersionIndex]; + + const explicitCompareVersion = compareWith + ? versions.find((version) => matchesVersion(version, compareWith)) + : undefined; + + const compareVersion = explicitCompareVersion; + + const compareTag = compareWith; + const currentTag = currentVersion ? versionTag(currentVersion) : build; + const activeCompareTag = + compareTag && + compareTag !== build && + compareTag !== currentTag && + compareTag !== currentVersion?.build + ? compareTag + : null; + + useEffect(() => { + let cancelled = false; + + if (!group || !activeCompareTag) { + setCompareKeys(null); + setCompareLoading(false); + setCompareError(false); + setChangesExpanded(false); + return; + } + + async function loadCompareKeys() { + const engine = await createEngine(group); + const baseKeys = await engine.getKeys(activeCompareTag!); + baseKeys.sort((a, b) => a.localeCompare(b)); + + if (!cancelled) { + setCompareKeys(baseKeys); + setCompareError(false); + } + } + + setCompareKeys(null); + setCompareLoading(true); + setCompareError(false); + loadCompareKeys() + .catch(() => { + if (!cancelled) { + setCompareKeys([]); + setCompareError(true); + setChangesExpanded(false); + } + }) + .finally(() => { + if (!cancelled) { + setCompareLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [group, activeCompareTag]); + const filtered = useMemo( () => keys.filter((key) => - key.toLowerCase().includes(debouncedKeyword.toLowerCase()) + key.toLowerCase().includes(debouncedKeyword.toLowerCase()), ), - [debouncedKeyword, keys] + [debouncedKeyword, keys], ); const topTokens = useMemo(() => {