diff --git a/apps/web/components/settings/admin/RecentImpersonationsList.tsx b/apps/web/components/settings/admin/RecentImpersonationsList.tsx new file mode 100644 index 0000000000..0a7c753c47 --- /dev/null +++ b/apps/web/components/settings/admin/RecentImpersonationsList.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useState } from "react"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { getRecentImpersonations, type RecentImpersonation } from "@calcom/lib/recentImpersonations"; +import { Button } from "@calcom/ui/components/button"; +import { PanelCard } from "@calcom/ui/components/card"; +import { + TableNew, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@calcom/ui/components/table"; + +interface RecentImpersonationsListProps { + onImpersonate?: (username: string) => void; +} + +export default function RecentImpersonationsList({ onImpersonate }: RecentImpersonationsListProps) { + const { t } = useLocale(); + const [recentImpersonations] = useState(() => getRecentImpersonations()); + + const handleQuickImpersonate = (username: string) => { + if (onImpersonate) { + onImpersonate(username); + } else { + signIn("impersonation-auth", { + username: username.toLowerCase(), + callbackUrl: `${WEBAPP_URL}/event-types`, + }); + } + }; + + if (recentImpersonations.length === 0) { + return null; + } + + return ( + +
+ + + + User + Impersonated At + Actions + + + + {recentImpersonations.map((impersonation) => ( + + {impersonation.username} + + {new Date(impersonation.timestamp).toLocaleDateString()}{" "} + {new Date(impersonation.timestamp).toLocaleTimeString()} + + + + + + ))} + + +
+
+ ); +} diff --git a/apps/web/modules/settings/admin/impersonation-view.tsx b/apps/web/modules/settings/admin/impersonation-view.tsx index 780b011ba7..c8c921da9f 100644 --- a/apps/web/modules/settings/admin/impersonation-view.tsx +++ b/apps/web/modules/settings/admin/impersonation-view.tsx @@ -2,23 +2,29 @@ import { signIn } from "next-auth/react"; import { useSearchParams } from "next/navigation"; -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState } from "react"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { TextField } from "@calcom/ui/components/form"; +import { addRecentImpersonation } from "@calcom/lib/recentImpersonations"; import { Button } from "@calcom/ui/components/button"; +import { PanelCard } from "@calcom/ui/components/card"; +import { TextField } from "@calcom/ui/components/form"; + +import RecentImpersonationsList from "../../../components/settings/admin/RecentImpersonationsList"; const ImpersonationView = () => { const { t } = useLocale(); const usernameRef = useRef(null); const searchParams = useSearchParams(); + const [refreshKey, setRefreshKey] = useState(0); const username = searchParams?.get("username")?.toLowerCase(); useEffect(() => { if (username) { const enteredUsername = username.toLowerCase(); + addRecentImpersonation(enteredUsername); signIn("impersonation-auth", { username: enteredUsername, callbackUrl: `${WEBAPP_URL}/event-types`, @@ -26,32 +32,52 @@ const ImpersonationView = () => { } }, [username]); + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const enteredUsername = usernameRef.current?.value.toLowerCase(); + if (enteredUsername) { + addRecentImpersonation(enteredUsername); + setRefreshKey((prev) => prev + 1); + signIn("impersonation-auth", { + username: enteredUsername, + callbackUrl: `${WEBAPP_URL}/event-types`, + }); + } + }; + + const handleQuickImpersonate = (username: string) => { + if (usernameRef.current) { + usernameRef.current.value = username; + } + addRecentImpersonation(username); + setRefreshKey((prev) => prev + 1); + signIn("impersonation-auth", { + username: username.toLowerCase(), + callbackUrl: `${WEBAPP_URL}/event-types`, + }); + }; + return ( -
{ - e.preventDefault(); - const enteredUsername = usernameRef.current?.value.toLowerCase(); - signIn("impersonation-auth", { - username: enteredUsername, - callbackUrl: `${WEBAPP_URL}/event-types`, - }); - }}> -
- {process.env.NEXT_PUBLIC_WEBSITE_URL}/} - ref={usernameRef} - hint={t("impersonate_user_tip")} - defaultValue={undefined} - data-testid="admin-impersonation-input" - /> - -
-
+
+ +
+
+ {process.env.NEXT_PUBLIC_WEBSITE_URL}/} + ref={usernameRef} + defaultValue={undefined} + data-testid="admin-impersonation-input" + /> + +
+
+
+ +
); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 3309315462..acc740b6b8 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1645,6 +1645,9 @@ "embeds": "Embeds", "impersonation": "Impersonation", "impersonation_description": "Settings to manage user impersonation", + "recent_impersonations": "Recent Impersonations", + "quick_impersonate": "Quick Impersonate", + "no_recent_impersonations": "No recent impersonations", "users": "Users", "user": "User", "profile_description": "Manage settings for your {{appName}} profile", diff --git a/packages/lib/recentImpersonations.ts b/packages/lib/recentImpersonations.ts new file mode 100644 index 0000000000..6f6e919672 --- /dev/null +++ b/packages/lib/recentImpersonations.ts @@ -0,0 +1,37 @@ +import { localStorage } from "@calcom/lib/webstorage"; + +export interface RecentImpersonation { + username: string; + timestamp: number; +} + +const STORAGE_KEY = "cal-recent-impersonations"; +const MAX_RECENT = 5; + +export function getRecentImpersonations(): RecentImpersonation[] { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) return []; + return JSON.parse(stored); + } catch { + return []; + } +} + +export function addRecentImpersonation(usernameRaw: string): void { + try { + const recent = getRecentImpersonations(); + const username = usernameRaw.trim().toLowerCase(); + if (!username) return; + const filtered = recent.filter((item) => item.username !== username); + const updated = [{ username, timestamp: Date.now() }, ...filtered].slice(0, MAX_RECENT); + + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + } catch {} +} + +export function clearRecentImpersonations(): void { + try { + localStorage.removeItem(STORAGE_KEY); + } catch {} +}