diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/client.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/client.tsx index a8f7c41b0b..4a81afa0aa 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/client.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/client.tsx @@ -1,6 +1,5 @@ "use client"; -import { PageHeader } from "@/components/dashboard/page-header"; import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; import { trpc } from "@/lib/trpc/client"; import { @@ -63,26 +62,6 @@ export function TeamPageClient({ team }: { team: boolean }) { return null; } - const actions: React.ReactNode[] = []; - - if (isAdmin) { - actions.push( - , - ); - - actions.push(); - } - if (!team) { return (
@@ -103,7 +82,24 @@ export function TeamPageClient({ team }: { team: boolean }) { return ( <> - + {isAdmin ? ( +
+
+ +
+ +
+ ) : null} {isLoading || !user || !organization || !userMemberships || !currentOrgMembership ? ( ) : tab === "members" ? ( diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/members.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/members.tsx index 18047823eb..291adab8e2 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/members.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/settings/team/members.tsx @@ -1,6 +1,5 @@ "use client"; -import { Confirm } from "@/components/dashboard/confirm"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Table, @@ -12,8 +11,8 @@ import { } from "@/components/ui/table"; import type { AuthenticatedUser, Membership, Organization } from "@/lib/auth/types"; import { trpc } from "@/lib/trpc/client"; -import { Button, Empty, Loading, toast } from "@unkey/ui"; -import { memo } from "react"; +import { Button, ConfirmPopover, Empty, Loading, toast } from "@unkey/ui"; +import { memo, useMemo, useState } from "react"; import { InviteButton } from "./invite"; import { RoleSwitcher } from "./role-switcher"; @@ -24,11 +23,16 @@ type MembersProps = { }; export const Members = memo(({ organization, user, userMembership }) => { + const [isConfirmPopoverOpen, setIsConfirmPopoverOpen] = useState(false); + const [currentMembership, setCurrentMembership] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); const { data: orgMemberships, isLoading } = trpc.org.members.list.useQuery(organization?.id); const memberships = orgMemberships?.data; const isAdmin = userMembership?.role === "admin"; const utils = trpc.useUtils(); + const anchorRef = useMemo(() => ({ current: anchorEl }), [anchorEl]); + const removeMember = trpc.org.members.remove.useMutation({ onSuccess: () => { // Invalidate the member list query to trigger a refetch @@ -48,6 +52,15 @@ export const Members = memo(({ organization, user, userMembership ); } + const handleDeleteButtonClick = ( + membership: Membership, + event: React.MouseEvent, + ) => { + setAnchorEl(event.currentTarget); + setCurrentMembership(membership); + setIsConfirmPopoverOpen(true); + }; + if (!memberships || memberships.length === 0) { return ( @@ -59,69 +72,89 @@ export const Members = memo(({ organization, user, userMembership } return ( - - - - Member - Role - {/*/ empty */} - - - - {memberships.map(({ id, role, user: member }) => ( - - -
- - - - {member.fullName?.slice(0, 1) ?? member.email.slice(0, 1)} - - -
- - {`${member.firstName ? member.firstName : member.email} ${ - member.lastName ? member.lastName : "" - }`} - - - {member.firstName ? member.email : ""} - -
-
-
- - - - - {isAdmin && user && member.id !== user.id ? ( - { - try { - await removeMember.mutateAsync({ - orgId: organization.id, - membershipId: id, - }); - } catch (error) { - console.error("Error removing member:", error); - } - }} - trigger={(onClick) => } - /> - ) : null} - + <> + {currentMembership && anchorEl ? ( + { + try { + await removeMember.mutateAsync({ + orgId: organization.id, + membershipId: currentMembership.id, + }); + } catch (error) { + console.error("Error removing member:", error); + } + }} + /> + ) : null} +
+ + + Member + Role + {/*/ empty */} - ))} - -
+ + + {memberships.map((membership) => { + const { id, role, user: member } = membership; + return ( + + +
+ + + + {member.fullName?.slice(0, 1) ?? member.email.slice(0, 1)} + + +
+ + {`${member.firstName ? member.firstName : member.email} ${ + member.lastName ? member.lastName : "" + }`} + + + {member.firstName ? member.email : ""} + +
+
+
+ + + + + {isAdmin && user && member.id !== user.id ? ( + + ) : null} + +
+ ); + })} +
+ + ); }); diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/client.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/client.tsx index a2459585e0..bef0cde6ee 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/client.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/client.tsx @@ -4,7 +4,6 @@ */ "use client"; -import { PageHeader } from "@/components/dashboard/page-header"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, @@ -91,19 +90,6 @@ export const Client: React.FC = ({ projects, integration, apis, rootKeys return ( <> - - - , - ]} - />
    {projects.map((project) => { diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/page.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/page.tsx index aff7e031e7..4897594852 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/page.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/settings/vercel/page.tsx @@ -3,7 +3,6 @@ * Hiding for now until we decide if we want to fix it up or toss it */ -import { Navbar as SubMenu } from "@/components/dashboard/navbar"; import { Navigation } from "@/components/navigation/navigation"; import { PageContent } from "@/components/page-content"; import { getAuth } from "@/lib/auth"; @@ -14,7 +13,6 @@ import { Button, Code, Empty } from "@unkey/ui"; import { Vercel } from "@unkey/vercel"; import Link from "next/link"; import { notFound } from "next/navigation"; -import { navigation } from "../constants"; import { Client } from "./client"; export const dynamic = "force-dynamic"; @@ -58,7 +56,6 @@ export default async function Page(props: Props) {
    } /> -
    Vercel is not connected to this workspace @@ -184,7 +181,6 @@ export default async function Page(props: Props) {
    } name="Settings" /> -
    diff --git a/apps/dashboard/app/integrations/vercel/callback/client.tsx b/apps/dashboard/app/integrations/vercel/callback/client.tsx index f73ad29c84..1b5b72b1fa 100644 --- a/apps/dashboard/app/integrations/vercel/callback/client.tsx +++ b/apps/dashboard/app/integrations/vercel/callback/client.tsx @@ -4,7 +4,6 @@ */ "use client"; -import { PageHeader } from "@/components/dashboard/page-header"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { trpc } from "@/lib/trpc/client"; @@ -21,7 +20,7 @@ import { } from "@unkey/ui"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import { WorkspaceSwitcher } from "./workspace"; +// import { WorkspaceSwitcher } from "./workspace"; type Props = { projects: { id: string; name: string }[]; apis: Api[]; @@ -70,12 +69,6 @@ export const Client: React.FC = ({ return (
    - ]} - /> -
    diff --git a/apps/dashboard/components/dashboard/charts.tsx b/apps/dashboard/components/dashboard/charts.tsx deleted file mode 100644 index a0a3b1f189..0000000000 --- a/apps/dashboard/components/dashboard/charts.tsx +++ /dev/null @@ -1,409 +0,0 @@ -"use client"; - -import { Area, Bar, Column, Line } from "@ant-design/plots"; -import { useTheme } from "next-themes"; - -type ColorName = "primary" | "warn" | "danger"; - -export const useColors = (colorNames: Array) => { - const { resolvedTheme } = useTheme(); - - const colors: { - light: Record; - dark: Record; - } = { - light: { - primary: "#1c1917", - warn: "#FFCD07", - danger: "#D12542", - }, - dark: { - primary: "#f1efef", - warn: "#FFE41C", - danger: "#FF7568", - }, - }; - - return { - color: resolvedTheme === "dark" ? "#f1efef" : "#1c1917", - palette: - resolvedTheme === "dark" - ? colorNames.map((c) => colors.dark[c]) - : colorNames.map((c) => colors.light[c]), - axisColor: resolvedTheme === "dark" ? "#1b1918" : "#e8e5e3", - }; -}; - -export type Props = { - data: { - x: string; - y: number; - }[]; - timeGranularity: "hour" | "day" | "month"; - tooltipLabel: string; - colors?: Array; - padding?: number[] | number | "auto"; -}; - -export const AreaChart: React.FC = ({ data, timeGranularity, tooltipLabel, padding }) => { - const { color, axisColor } = useColors(["primary", "warn", "danger"]); - return ( - { - switch (timeGranularity) { - case "hour": - return new Date(v).toLocaleTimeString(); - case "day": - return new Date(v).toLocaleDateString(); - case "month": - return new Date(v).toLocaleDateString(undefined, { - month: "long", - year: "numeric", - }); - } - }, - }, - }} - yAxis={{ - tickCount: 3, - label: { - formatter: (v: string) => - Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(v)), - }, - grid: { - line: { - style: { - stroke: axisColor, - }, - }, - }, - }} - tooltip={{ - formatter: (datum) => ({ - name: tooltipLabel, - value: Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(datum.y)), - }), - }} - /> - ); -}; - -export const LineChart: React.FC<{ - data: { - category: string; - x: string; - y: number; - }[]; -}> = ({ data }) => { - return ( - ({ - name: datum.category, - value: `${Intl.NumberFormat(undefined, { - notation: "compact", - }).format(Number(datum.y))} ms`, - }), - }} - /> - ); -}; - -export const ColumnChart: React.FC = ({ data, colors }) => { - const { color, axisColor } = useColors(colors ?? ["primary", "warn", "danger"]); - return ( - new Date(v).toLocaleString(), - }, - tickLine: { - style: { - stroke: axisColor, - }, - }, - line: { - style: { - stroke: axisColor, - }, - }, - }} - yAxis={{ - tickCount: 5, - label: { - formatter: (v: string) => - Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(v)), - }, - grid: { - line: { - style: { - stroke: axisColor, - }, - }, - }, - }} - tooltip={{ - formatter: (datum) => ({ - name: "Usage", - value: Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(datum.y)), - }), - }} - /> - ); -}; - -export const StackedColumnChart: React.FC<{ - data: { - category: string; - x: string; - y: number; - }[]; - timeGranularity?: "minute" | "hour" | "day" | "month"; - colors: Array; -}> = ({ data, timeGranularity, colors }) => { - const { axisColor } = useColors(colors); - - const formatDate = (date: string) => { - const d = new Date(date); - if (Number.isNaN(d.getTime())) { - return date; - } - - switch (timeGranularity) { - case "minute": - return d.toLocaleString(undefined, { - hour: "numeric", - minute: "2-digit", - hour12: true, - month: "short", - day: "numeric", - }); - case "hour": - return d.toLocaleString(undefined, { - hour: "numeric", - hour12: true, - month: "short", - day: "numeric", - year: "numeric", - }); - case "day": - return d.toLocaleString(undefined, { - weekday: "short", - month: "short", - day: "numeric", - year: "numeric", - }); - case "month": - return d.toLocaleString(undefined, { - month: "long", - year: "numeric", - }); - default: - return d.toLocaleString(undefined, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "2-digit", - }); - } - }; - - return ( - { - return { - fill: "rgba(0,0,0,0.25)", - stroke: oldStyle.fill, - lineWidth: 0.5, - }; - }, - }} - xAxis={{ - label: { - formatter: (v: string) => { - switch (timeGranularity) { - case "minute": - return new Date(v).toLocaleTimeString(); - case "hour": - return new Date(v).toLocaleTimeString(); - case "day": - return new Date(v).toLocaleDateString(); - case "month": - return new Date(v).toLocaleDateString(undefined, { - month: "long", - year: "numeric", - }); - default: - return v; - } - }, - }, - tickLine: { - style: { - stroke: axisColor, - }, - }, - line: { - style: { - stroke: axisColor, - }, - }, - }} - yAxis={{ - tickCount: 5, - label: { - formatter: (v: string) => - Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(v)), - }, - grid: { - line: { - style: { - stroke: axisColor, - }, - }, - }, - }} - tooltip={{ - title: formatDate, - formatter: (datum) => ({ - name: datum.category, - value: Intl.NumberFormat(undefined, { - notation: "compact", - maximumFractionDigits: 1, - compactDisplay: "short", - }).format(Number(datum.y)), - }), - }} - /> - ); -}; - -export const StackedBarChart: React.FC<{ - data: { - category: string; - x: number; - y: string; - }[]; - colors: Array; -}> = ({ data, colors }) => { - const { palette, axisColor } = useColors(colors); - return ( - - d.x > 0 ? Intl.NumberFormat(undefined, { notation: "compact" }).format(d.x) : "", - }} - maxBarWidth={16} - yAxis={{ - label: { - formatter: (v: string) => { - return v.length <= 16 ? v : `${v.slice(0, 16)}...`; - }, - }, - tickLine: { - style: { - stroke: axisColor, - }, - }, - line: { - style: { - stroke: axisColor, - }, - }, - }} - xAxis={{ - tickCount: 5, - label: { - formatter: (v: string) => - Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(v)), - }, - grid: { - line: { - style: { - stroke: axisColor, - }, - }, - }, - }} - tooltip={{ - formatter: (datum) => ({ - name: datum.category, - value: Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(datum.x)), - }), - }} - /> - ); -}; diff --git a/apps/dashboard/components/dashboard/confirm.tsx b/apps/dashboard/components/dashboard/confirm.tsx deleted file mode 100644 index 3ad2c9fbfc..0000000000 --- a/apps/dashboard/components/dashboard/confirm.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import { Button, DialogContainer } from "@unkey/ui"; -import type React from "react"; -import { useState } from "react"; - -export type ConfirmProps = { - title: string; - description?: string; - trigger: (onClick: () => void) => React.ReactNode; - onConfirm: () => void | Promise; - variant?: "destructive"; - disabled?: boolean; -}; - -export const Confirm: React.FC = (props): JSX.Element => { - const [isOpen, setIsOpen] = useState(false); - - const [loading, setLoading] = useState(false); - - const onConfirm = async () => { - setLoading(true); - await props.onConfirm(); - setLoading(false); - setIsOpen(false); - }; - - return ( - <> - {props.trigger(() => setIsOpen(true))} - - -
    - } - > -

    {props.description}

    - - - ); -}; diff --git a/apps/dashboard/components/dashboard/navbar.tsx b/apps/dashboard/components/dashboard/navbar.tsx deleted file mode 100644 index a47b1936ad..0000000000 --- a/apps/dashboard/components/dashboard/navbar.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import Link from "next/link"; -import * as React from "react"; - -import { cn } from "@/lib/utils"; -import { Separator } from "@unkey/ui"; -import { useRouter, useSelectedLayoutSegment } from "next/navigation"; - -type Props = { - navigation: { - label: string; - href: string; - segment: string | null; - tag?: string; - isActive?: boolean; - }[]; - segment?: string; - className?: string; -}; - -export const Navbar: React.FC> = ({ - navigation, - className, - segment, -}) => { - return ( - - ); -}; - -const NavItem: React.FC = ({ label, href, segment, tag, isActive }) => { - const selectedSegment = useSelectedLayoutSegment(); - const [isPending, startTransition] = React.useTransition(); - const router = useRouter(); - - const active = segment === selectedSegment || isActive; - - return ( -
  • - - startTransition(() => { - router.push(href); - }) - } - className={cn( - "text-sm flex items-center gap-1 font-medium px-3 -mx-3 text-content-subtle hover:bg-background-subtle rounded-md hover:text-primary", - { - "text-primary": active, - }, - )} - > - {label} - {tag ? ( -
    - {tag} -
    - ) : null} - -
  • - ); -}; diff --git a/apps/dashboard/components/dashboard/page-header.tsx b/apps/dashboard/components/dashboard/page-header.tsx deleted file mode 100644 index da9c3d9582..0000000000 --- a/apps/dashboard/components/dashboard/page-header.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { cn } from "@/lib/utils"; -import type React from "react"; - -type Props = { - title: React.ReactNode; - description?: string; - /** - * A set of components displayed in the top right - * null components are filtered out - */ - actions?: React.ReactNode[]; - /** - * Additional classes to be applied to the root element - */ - className?: string; -}; - -export const PageHeader: React.FC = ({ title, description, actions, className }) => { - const actionRows: React.ReactNode[][] = []; - if (actions) { - for (let i = 0; i < actions.length; i += 3) { - actionRows.push(actions.slice(i, i + 3)); - } - } - - return ( -
    -
    -

    {title}

    -

    {description}

    -
    - {actionRows.map((row, i) => ( -
      - {row.map((action, i) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: I got nothing better right now -
    • {action}
    • - ))} -
    - ))} -
    - ); -}; diff --git a/apps/dashboard/components/logs/llm-search/components/search-actions.tsx b/apps/dashboard/components/logs/llm-search/components/search-actions.tsx deleted file mode 100644 index 0aa015464f..0000000000 --- a/apps/dashboard/components/logs/llm-search/components/search-actions.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { XMark } from "@unkey/icons"; -import { SearchExampleTooltip } from "./search-example-tooltip"; - -type SearchActionsProps = { - exampleQueries?: string[]; - searchText: string; - hideClear: boolean; - hideExplainer: boolean; - isProcessing: boolean; - searchMode: "allowTypeDuringSearch" | "debounced" | "manual"; - onClear: () => void; - onSelectExample: (query: string) => void; -}; - -/** - * SearchActions component renders the right-side actions (clear button or examples tooltip) - */ -export const SearchActions: React.FC = ({ - exampleQueries, - searchText, - hideClear, - hideExplainer, - isProcessing, - searchMode, - onClear, - onSelectExample, -}) => { - // Don't render anything if processing (unless in allowTypeDuringSearch mode) - if (!(!isProcessing || searchMode === "allowTypeDuringSearch")) { - return null; - } - - // Render clear button when there's text - if (searchText.length > 0 && !hideClear) { - return ( - - ); - } - - if (searchText.length === 0 && !hideExplainer) { - return ( - - ); - } - - return null; -}; diff --git a/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx b/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx deleted file mode 100644 index 56e23caf33..0000000000 --- a/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { CaretRightOutline, CircleInfoSparkle } from "@unkey/icons"; -import { InfoTooltip } from "@unkey/ui"; - -type SearchExampleTooltipProps = { - onSelectExample: (query: string) => void; - exampleQueries?: string[]; -}; - -export const SearchExampleTooltip: React.FC = ({ - onSelectExample, - exampleQueries, -}) => { - const examples = exampleQueries ?? [ - "Show failed requests today", - "auth errors in the last 3h", - "API calls from a path that includes /api/v1/oz", - ]; - - return ( - -
    - Try queries like: - (click to use) -
    -
      - {examples.map((example) => ( -
    • - - -
    • - ))} -
    -
    - } - delayDuration={150} - > -
    - -
    - - ); -}; diff --git a/apps/dashboard/components/logs/llm-search/components/search-icon.tsx b/apps/dashboard/components/logs/llm-search/components/search-icon.tsx deleted file mode 100644 index 3bd93ec221..0000000000 --- a/apps/dashboard/components/logs/llm-search/components/search-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Magnifier, Refresh3 } from "@unkey/icons"; - -type SearchIconProps = { - isProcessing: boolean; -}; - -export const SearchIcon = ({ isProcessing }: SearchIconProps) => { - if (isProcessing) { - return ; - } - - return ; -}; diff --git a/apps/dashboard/components/logs/llm-search/components/search-input.tsx b/apps/dashboard/components/logs/llm-search/components/search-input.tsx deleted file mode 100644 index 6d47accfdc..0000000000 --- a/apps/dashboard/components/logs/llm-search/components/search-input.tsx +++ /dev/null @@ -1,50 +0,0 @@ -type SearchInputProps = { - value: string; - placeholder: string; - isProcessing: boolean; - isLoading: boolean; - loadingText: string; - clearingText: string; - searchMode: "allowTypeDuringSearch" | "debounced" | "manual"; - onChange: (e: React.ChangeEvent) => void; - onKeyDown: (e: React.KeyboardEvent) => void; - inputRef: React.RefObject; -}; - -const LLM_LIMITS_MAX_QUERY_LENGTH = 120; -export const SearchInput = ({ - value, - placeholder, - isProcessing, - isLoading, - loadingText, - clearingText, - searchMode, - onChange, - onKeyDown, - inputRef, -}: SearchInputProps) => { - // Show loading state unless we're in allowTypeDuringSearch mode - if (isProcessing && searchMode !== "allowTypeDuringSearch") { - return ( -
    - {isLoading ? loadingText : clearingText} -
    - ); - } - - return ( - - ); -}; diff --git a/apps/dashboard/components/logs/llm-search/hooks/use-search-strategy.test.tsx b/apps/dashboard/components/logs/llm-search/hooks/use-search-strategy.test.tsx deleted file mode 100644 index 07501d8098..0000000000 --- a/apps/dashboard/components/logs/llm-search/hooks/use-search-strategy.test.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { act, renderHook } from "@testing-library/react-hooks"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { useSearchStrategy } from "./use-search-strategy"; - -describe("useSearchStrategy", () => { - // Mock timers for debounce/throttle testing - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - const onSearchMock = vi.fn(); - - it("should execute search immediately with executeSearch", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - act(() => { - result.current.executeSearch("test query"); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith("test query"); - }); - - it("should not execute search with empty query", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - act(() => { - result.current.executeSearch(" "); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - }); - - it("should debounce search calls with debouncedSearch", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - act(() => { - result.current.debouncedSearch("test query"); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(499); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(1); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith("test query"); - }); - - it("should cancel previous debounce if debouncedSearch is called again", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - act(() => { - result.current.debouncedSearch("first query"); - }); - - act(() => { - vi.advanceTimersByTime(300); - }); - - act(() => { - result.current.debouncedSearch("second query"); - }); - - act(() => { - vi.advanceTimersByTime(300); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(200); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith("second query"); - expect(onSearchMock).not.toHaveBeenCalledWith("first query"); - }); - - it("should use debounce for initial query with throttledSearch", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - act(() => { - result.current.throttledSearch("initial query"); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(500); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith("initial query"); - }); - - it("should throttle subsequent searches", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - // First search - should be debounced - act(() => { - result.current.throttledSearch("initial query"); - vi.advanceTimersByTime(500); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - - // Reset mock to track subsequent calls - onSearchMock.mockReset(); - - // Second search immediately after - should be throttled - act(() => { - result.current.throttledSearch("second query"); - }); - - // Should not execute immediately due to throttling - expect(onSearchMock).not.toHaveBeenCalled(); - - // Advance time to just before throttle interval ends - act(() => { - vi.advanceTimersByTime(999); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - - // Complete the throttle interval - act(() => { - vi.advanceTimersByTime(1); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith("second query"); - }); - - it("should clean up timers with clearDebounceTimer", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - act(() => { - result.current.debouncedSearch("test query"); - }); - - act(() => { - result.current.clearDebounceTimer(); - }); - - act(() => { - vi.advanceTimersByTime(1000); - }); - - expect(onSearchMock).not.toHaveBeenCalled(); - }); - - it("should reset search state with resetSearchState", () => { - const { result } = renderHook(() => useSearchStrategy(onSearchMock, 500)); - - // First search to set initial state - act(() => { - result.current.throttledSearch("initial query"); - vi.advanceTimersByTime(500); - }); - - onSearchMock.mockReset(); - - // Reset search state - act(() => { - result.current.resetSearchState(); - }); - - // Next search should be debounced again, not throttled - act(() => { - result.current.throttledSearch("new query after reset"); - }); - - // Should not execute immediately (debounced, not throttled) - expect(onSearchMock).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(500); - }); - - expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith("new query after reset"); - }); -}); diff --git a/apps/dashboard/components/logs/llm-search/hooks/use-search-strategy.ts b/apps/dashboard/components/logs/llm-search/hooks/use-search-strategy.ts deleted file mode 100644 index c44ff130a4..0000000000 --- a/apps/dashboard/components/logs/llm-search/hooks/use-search-strategy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { useCallback, useRef } from "react"; - -/** - * Custom hook that provides different search strategies - * @param onSearch Function to execute the search - * @param debounceTime Delay for debounce in ms - */ -export const useSearchStrategy = (onSearch: (query: string) => void, debounceTime = 500) => { - const debounceTimerRef = useRef(null); - const lastSearchTimeRef = useRef(0); - const THROTTLE_INTERVAL = 1000; - - /** - * Clears the debounce timer - */ - const clearDebounceTimer = useCallback(() => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - debounceTimerRef.current = null; - } - }, []); - - /** - * Executes the search with the given query - */ - const executeSearch = useCallback( - (query: string) => { - if (query.trim()) { - try { - lastSearchTimeRef.current = Date.now(); - onSearch(query.trim()); - } catch (error) { - console.error("Search failed:", error); - } - } - }, - [onSearch], - ); - - /** - * Debounced search - waits for user to stop typing before executing search - */ - const debouncedSearch = useCallback( - (search: string) => { - clearDebounceTimer(); - - debounceTimerRef.current = setTimeout(() => { - executeSearch(search); - }, debounceTime); - }, - [clearDebounceTimer, executeSearch, debounceTime], - ); - - /** - * Throttled search with initial debounce - debounce first query, throttle subsequent searches - */ - - const throttledSearch = useCallback( - (search: string) => { - const now = Date.now(); - const timeElapsed = now - lastSearchTimeRef.current; - const query = search.trim(); - - // If this is the first search, use debounced search - if (lastSearchTimeRef.current === 0 && query) { - debouncedSearch(search); - return; - } - - // For subsequent searches, use throttling - if (timeElapsed >= THROTTLE_INTERVAL) { - // Enough time has passed, execute immediately - executeSearch(search); - } else if (query) { - // Not enough time has passed, schedule for later - clearDebounceTimer(); - - // Schedule execution after remaining throttle time - const remainingTime = THROTTLE_INTERVAL - timeElapsed; - debounceTimerRef.current = setTimeout(() => { - throttledSearch(search); - }, remainingTime); - } - }, - [clearDebounceTimer, debouncedSearch, executeSearch], - ); - - /** - * Resets search state for new search sequences - */ - const resetSearchState = useCallback(() => { - lastSearchTimeRef.current = 0; - }, []); - - return { - debouncedSearch, - throttledSearch, - executeSearch, - clearDebounceTimer, - resetSearchState, - }; -}; diff --git a/apps/dashboard/components/logs/llm-search/index.tsx b/apps/dashboard/components/logs/llm-search/index.tsx deleted file mode 100644 index 30d6b2844c..0000000000 --- a/apps/dashboard/components/logs/llm-search/index.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; -import { cn } from "@/lib/utils"; -import { useEffect, useRef, useState } from "react"; -import { SearchActions } from "./components/search-actions"; -import { SearchIcon } from "./components/search-icon"; -import { SearchInput } from "./components/search-input"; -import { useSearchStrategy } from "./hooks/use-search-strategy"; - -type SearchMode = "allowTypeDuringSearch" | "debounced" | "manual"; - -type Props = { - exampleQueries?: string[]; - onSearch: (query: string) => void; - onClear?: () => void; - placeholder?: string; - isLoading: boolean; - hideExplainer?: boolean; - hideClear?: boolean; - loadingText?: string; - clearingText?: string; - searchMode?: SearchMode; - debounceTime?: number; -}; - -export const LogsLLMSearch = ({ - exampleQueries, - onSearch, - isLoading, - onClear, - hideExplainer = false, - hideClear = false, - placeholder = "Search and filter with AI…", - loadingText = "AI consults the Palantír...", - clearingText = "Clearing search...", - searchMode = "manual", - debounceTime = 500, -}: Props) => { - const [searchText, setSearchText] = useState(""); - const [isClearingState, setIsClearingState] = useState(false); - - const inputRef = useRef(null); - - const isClearing = isClearingState; - const isProcessing = isLoading || isClearing; - - const { debouncedSearch, throttledSearch, executeSearch, clearDebounceTimer, resetSearchState } = - useSearchStrategy(onSearch, debounceTime); - useKeyboardShortcut("s", () => { - inputRef.current?.click(); - inputRef.current?.focus(); - }); - - const handleClear = () => { - clearDebounceTimer(); - setIsClearingState(true); - - setTimeout(() => { - onClear?.(); - setSearchText(""); - }, 0); - - setIsClearingState(false); - resetSearchState(); - }; - - const handleInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - const wasFilled = searchText !== ""; - - setSearchText(value); - - // Handle clearing - if (wasFilled && value === "") { - handleClear(); - return; - } - - // Skip if empty - if (value === "") { - return; - } - - // Apply appropriate search strategy based on mode - switch (searchMode) { - case "allowTypeDuringSearch": - throttledSearch(value); - break; - case "debounced": - debouncedSearch(value); - break; - case "manual": - // Do nothing - search triggered on Enter key or preset click - break; - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - setSearchText(""); - handleClear(); - inputRef.current?.blur(); - } - - if (e.key === "Enter") { - e.preventDefault(); - if (searchText !== "") { - executeSearch(searchText); - } else { - handleClear(); - } - } - }; - - const handlePresetQuery = (query: string) => { - setSearchText(query); - executeSearch(query); - }; - - // Clean up timers on unmount - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - return clearDebounceTimer(); - }, []); - - return ( -
    -
    0 ? "bg-gray-4" : "", - isProcessing ? "bg-gray-4" : "", - )} - > -
    -
    - -
    - -
    - -
    -
    - - -
    -
    - ); -}; diff --git a/apps/dashboard/components/ui/calendar.tsx b/apps/dashboard/components/ui/calendar.tsx deleted file mode 100644 index 43da2d2ccb..0000000000 --- a/apps/dashboard/components/ui/calendar.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { ChevronLeft, ChevronRight } from "@unkey/icons"; -import type * as React from "react"; -import { DayPicker } from "react-day-picker"; - -import { cn } from "@/lib/utils"; -import { buttonVariants } from "@unkey/ui"; - -export type CalendarProps = React.ComponentProps; - -function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { - return ( - , - IconRight: () => , - }} - {...props} - /> - ); -} -Calendar.displayName = "Calendar"; - -export { Calendar }; diff --git a/apps/dashboard/components/ui/code.tsx b/apps/dashboard/components/ui/code.tsx deleted file mode 100644 index 2aa5c678ac..0000000000 --- a/apps/dashboard/components/ui/code.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { type VariantProps, cva } from "class-variance-authority"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -const codeVariants = cva( - "inline-flex font-mono items-center rounded-md border border-border bg-transparent px-2.5 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: " text-primary bg-background-subtle hover:border-primary", - - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -export interface CodeProps - extends React.HTMLAttributes, - VariantProps {} - -function Code({ className, variant, ...props }: CodeProps) { - return
    ;
    -}
    -
    -export { Code, codeVariants };
    diff --git a/apps/dashboard/components/ui/metric.tsx b/apps/dashboard/components/ui/metric.tsx
    deleted file mode 100644
    index 73378a8d73..0000000000
    --- a/apps/dashboard/components/ui/metric.tsx
    +++ /dev/null
    @@ -1,24 +0,0 @@
    -import { cn } from "@/lib/utils";
    -import type * as React from "react";
    -
    -interface MetricProps {
    -  label: string;
    -  value: string | React.ReactNode;
    -  className?: string;
    -}
    -
    -const Metric: React.FC = ({ label, value, className }) => {
    -  return (
    -    
    -

    {label}

    -
    {value}
    -
    - ); -}; - -export { Metric }; diff --git a/apps/dashboard/components/ui/shiny-text.tsx b/apps/dashboard/components/ui/shiny-text.tsx deleted file mode 100644 index b3cf74a18b..0000000000 --- a/apps/dashboard/components/ui/shiny-text.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { CSSProperties, ComponentPropsWithoutRef, FC } from "react"; - -import { cn } from "@/lib/utils"; - -export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<"span"> { - shimmerWidth?: number; - textColor?: string; - gradientColor?: string; -} - -export const AnimatedShinyText: FC = ({ - children, - className, - shimmerWidth = 100, - textColor, - gradientColor, - ...props -}) => { - return ( - - {children} - - ); -};