Openstatus www.openstatus.dev

feat: copy id for api visibility (#1250)

* chore: add copy id buttons

* fix: order

* feat: tooltip

authored by

Maximilian Kaske and committed by
GitHub
2e4517af 504b92e6

+105 -6
+2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/layout.tsx
··· 4 4 5 5 import { Header } from "@/components/dashboard/header"; 6 6 import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 7 + import { CopyButton } from "@/components/layout/header/copy-button"; 7 8 import { JobTypeIconWithTooltip } from "@/components/monitor/job-type-icon-with-tooltip"; 8 9 import { NotificationIconWithTooltip } from "@/components/monitor/notification-icon-with-tooltip"; 9 10 import { StatusDotWithTooltip } from "@/components/monitor/status-dot-with-tooltip"; ··· 82 83 /> 83 84 </div> 84 85 } 86 + actions={<CopyButton key="copy" id={monitor.id} />} 85 87 /> 86 88 {children} 87 89 </AppPageWithSidebarLayout>
+2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/layout.tsx
··· 2 2 3 3 import { Header } from "@/components/dashboard/header"; 4 4 import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 5 + import { CopyButton } from "@/components/layout/header/copy-button"; 5 6 import { api } from "@/trpc/server"; 6 7 7 8 export default async function Layout(props: { ··· 27 28 <Header 28 29 title={notification.name} 29 30 description={<span className="font-mono">{notification.provider}</span>} 31 + actions={<CopyButton key="copy" id={notification.id} />} 30 32 /> 31 33 {children} 32 34 </AppPageWithSidebarLayout>
+6 -4
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/layout.tsx
··· 6 6 import { getBaseUrl } from "@/app/status-page/[domain]/utils"; 7 7 import { Header } from "@/components/dashboard/header"; 8 8 import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 9 + import { CopyButton } from "@/components/layout/header/copy-button"; 9 10 import { api } from "@/trpc/server"; 10 11 11 12 export default async function Layout(props: { ··· 31 32 <Header 32 33 title={page.title} 33 34 description={page.description} 34 - actions={ 35 - <Button variant="outline" asChild> 35 + actions={[ 36 + <CopyButton key="copy" id={page.id} />, 37 + <Button key="visit" variant="outline" asChild> 36 38 <Link 37 39 target="_blank" 38 40 href={getBaseUrl({ ··· 42 44 > 43 45 Visit 44 46 </Link> 45 - </Button> 46 - } 47 + </Button>, 48 + ]} 47 49 /> 48 50 {children} 49 51 </AppPageWithSidebarLayout>
+11
apps/web/src/components/data-table/maintenance/data-table-row-actions.tsx
··· 25 25 } from "@openstatus/ui"; 26 26 27 27 import { LoadingAnimation } from "@/components/loading-animation"; 28 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 28 29 import { toastAction } from "@/lib/toast"; 29 30 import { api } from "@/trpc/client"; 30 31 ··· 39 40 const router = useRouter(); 40 41 const [alertOpen, setAlertOpen] = React.useState(false); 41 42 const [isPending, startTransition] = React.useTransition(); 43 + const { copy } = useCopyToClipboard(); 42 44 43 45 async function onDelete() { 44 46 startTransition(async () => { ··· 70 72 <Link href={`./maintenances/${maintenance.id}/edit`}> 71 73 <DropdownMenuItem>Edit</DropdownMenuItem> 72 74 </Link> 75 + <DropdownMenuItem 76 + onClick={() => 77 + copy(`${maintenance.id}`, { 78 + withToast: `Copied ID '${maintenance.id}'`, 79 + }) 80 + } 81 + > 82 + Copy ID 83 + </DropdownMenuItem> 73 84 <AlertDialogTrigger asChild> 74 85 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 75 86 Delete
+11
apps/web/src/components/data-table/monitor/data-table-row-actions.tsx
··· 32 32 import { api } from "@/trpc/client"; 33 33 34 34 import type { TCPResponse } from "@/app/api/checker/test/tcp/schema"; 35 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 35 36 36 37 interface DataTableRowActionsProps<TData> { 37 38 row: Row<TData>; ··· 46 47 const router = useRouter(); 47 48 const [alertOpen, setAlertOpen] = useState(false); 48 49 const [isPending, startTransition] = useTransition(); 50 + const { copy } = useCopyToClipboard(); 49 51 50 52 async function onDelete() { 51 53 startTransition(async () => { ··· 155 157 Clone 156 158 </DropdownMenuItem> 157 159 <DropdownMenuItem onClick={onTest}>Test</DropdownMenuItem> 160 + <DropdownMenuItem 161 + onClick={() => 162 + copy(`${monitor.id}`, { 163 + withToast: `Copied ID '${monitor.id}'`, 164 + }) 165 + } 166 + > 167 + Copy ID 168 + </DropdownMenuItem> 158 169 <DropdownMenuSeparator /> 159 170 <AlertDialogTrigger asChild> 160 171 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background">
+11
apps/web/src/components/data-table/notification/data-table-row-actions.tsx
··· 26 26 } from "@openstatus/ui"; 27 27 28 28 import { LoadingAnimation } from "@/components/loading-animation"; 29 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 29 30 import { toastAction } from "@/lib/toast"; 30 31 import { api } from "@/trpc/client"; 31 32 ··· 40 41 const router = useRouter(); 41 42 const [alertOpen, setAlertOpen] = React.useState(false); 42 43 const [isPending, startTransition] = React.useTransition(); 44 + const { copy } = useCopyToClipboard(); 43 45 44 46 async function onDelete() { 45 47 startTransition(async () => { ··· 73 75 <Link href={`./notifications/${notification.id}/edit`}> 74 76 <DropdownMenuItem>Edit</DropdownMenuItem> 75 77 </Link> 78 + <DropdownMenuItem 79 + onClick={() => 80 + copy(`${notification.id}`, { 81 + withToast: `Copied ID '${notification.id}'`, 82 + }) 83 + } 84 + > 85 + Copy ID 86 + </DropdownMenuItem> 76 87 <DropdownMenuSeparator /> 77 88 <AlertDialogTrigger asChild> 78 89 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background">
+9
apps/web/src/components/data-table/status-page/data-table-row-actions.tsx
··· 105 105 > 106 106 <DropdownMenuItem>Visit</DropdownMenuItem> 107 107 </Link> 108 + <DropdownMenuItem 109 + onClick={() => 110 + copy(`${page.id}`, { 111 + withToast: `Copied ID '${page.id}'`, 112 + }) 113 + } 114 + > 115 + Copy ID 116 + </DropdownMenuItem> 108 117 <DropdownMenuSeparator /> 109 118 <Link href={`./status-pages/${page.id}/reports/new`}> 110 119 <DropdownMenuItem>Create Report</DropdownMenuItem>
+11
apps/web/src/components/data-table/status-report/data-table-row-actions.tsx
··· 25 25 } from "@openstatus/ui"; 26 26 27 27 import { LoadingAnimation } from "@/components/loading-animation"; 28 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 28 29 import { toastAction } from "@/lib/toast"; 29 30 import { api } from "@/trpc/client"; 30 31 ··· 39 40 const router = useRouter(); 40 41 const [alertOpen, setAlertOpen] = React.useState(false); 41 42 const [isPending, startTransition] = React.useTransition(); 43 + const { copy } = useCopyToClipboard(); 42 44 43 45 async function onDelete() { 44 46 startTransition(async () => { ··· 75 77 <Link href={`./reports/${statusReport.id}/overview`}> 76 78 <DropdownMenuItem>View</DropdownMenuItem> 77 79 </Link> 80 + <DropdownMenuItem 81 + onClick={() => 82 + copy(`${statusReport.id}`, { 83 + withToast: `Copied ID '${statusReport.id}'`, 84 + }) 85 + } 86 + > 87 + Copy ID 88 + </DropdownMenuItem> 78 89 <AlertDialogTrigger asChild> 79 90 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 80 91 Delete
+36
apps/web/src/components/layout/header/copy-button.tsx
··· 1 + "use client"; 2 + 3 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 4 + import { 5 + Button, 6 + Tooltip, 7 + TooltipContent, 8 + TooltipProvider, 9 + TooltipTrigger, 10 + } from "@openstatus/ui"; 11 + import { Copy } from "lucide-react"; 12 + 13 + export function CopyButton({ id }: { id: string | number }) { 14 + const { copy } = useCopyToClipboard(); 15 + return ( 16 + <TooltipProvider> 17 + <Tooltip> 18 + <TooltipTrigger asChild> 19 + <Button 20 + variant="ghost" 21 + className="font-mono" 22 + onClick={() => { 23 + copy(`${id}`, { 24 + withToast: `Copied ID '${id}'`, 25 + }); 26 + }} 27 + > 28 + <Copy className="h-4 w-4 mr-2 text-muted-foreground" /> 29 + {id} 30 + </Button> 31 + </TooltipTrigger> 32 + <TooltipContent>Copy ID for API usage</TooltipContent> 33 + </Tooltip> 34 + </TooltipProvider> 35 + ); 36 + }
+6 -2
apps/web/src/hooks/use-copy-to-clipboard.ts
··· 10 10 { 11 11 timeout = 3000, 12 12 withToast = false, 13 - }: { timeout?: number; withToast?: boolean }, 13 + }: { timeout?: number; withToast?: boolean | string }, 14 14 ) => { 15 15 if (!navigator?.clipboard) { 16 16 console.warn("Clipboard not supported"); ··· 28 28 } 29 29 30 30 if (withToast) { 31 - toast.success("Copied to clipboard"); 31 + if (typeof withToast === "string") { 32 + toast.success(withToast); 33 + } else { 34 + toast.success("Copied to clipboard"); 35 + } 32 36 } 33 37 34 38 return true;