diff --git a/beszel/site/src/components/copy-to-clipboard.tsx b/beszel/site/src/components/copy-to-clipboard.tsx new file mode 100644 index 0000000..4e18688 --- /dev/null +++ b/beszel/site/src/components/copy-to-clipboard.tsx @@ -0,0 +1,49 @@ +import { useEffect, useMemo, useRef } from 'react' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog' +import { Textarea } from './ui/textarea' +import { $copyContent } from '@/lib/stores' + +export default function CopyToClipboard({ content }: { content: string }) { + return ( + + + + Could not copy to clipboard + Please copy the text manually. + + + + Clipboard API requires a secure context (https, localhost, or *.localhost) + + + + ) +} + +function CopyTextarea({ content }: { content: string }) { + const textareaRef = useRef(null) + + const rows = useMemo(() => { + return content.split('\n').length + }, [content]) + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.select() + } + }, [textareaRef]) + + useEffect(() => { + return () => $copyContent.set('') + }, []) + + return ( + + ) +} diff --git a/beszel/site/src/components/ui/textarea.tsx b/beszel/site/src/components/ui/textarea.tsx new file mode 100644 index 0000000..cf2329f --- /dev/null +++ b/beszel/site/src/components/ui/textarea.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +export interface TextareaProps extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ) + } +) +Textarea.displayName = 'Textarea' + +export { Textarea } diff --git a/beszel/site/src/lib/stores.ts b/beszel/site/src/lib/stores.ts index 1e03ba4..a80991a 100644 --- a/beszel/site/src/lib/stores.ts +++ b/beszel/site/src/lib/stores.ts @@ -25,3 +25,6 @@ export const $chartTime = atom('1h') as WritableAtom /** Container chart filter */ export const $containerFilter = atom('') + +/** Fallback copy to clipboard dialog content */ +export const $copyContent = atom('') diff --git a/beszel/site/src/lib/utils.ts b/beszel/site/src/lib/utils.ts index df1f4a6..c2f15bc 100644 --- a/beszel/site/src/lib/utils.ts +++ b/beszel/site/src/lib/utils.ts @@ -1,7 +1,7 @@ import { toast } from '@/components/ui/use-toast' import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' -import { $alerts, $systems, pb } from './stores' +import { $alerts, $copyContent, $systems, pb } from './stores' import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types' import { RecordModel, RecordSubscription } from 'pocketbase' import { WritableAtom } from 'nanostores' @@ -22,10 +22,7 @@ export async function copyToClipboard(content: string) { description: 'Copied to clipboard', }) } catch (e: any) { - prompt( - 'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:', - content - ) + $copyContent.set(content) } } diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx index 6e9bc44..f32ad68 100644 --- a/beszel/site/src/main.tsx +++ b/beszel/site/src/main.tsx @@ -3,7 +3,14 @@ import React, { Suspense, lazy, useEffect } from 'react' import ReactDOM from 'react-dom/client' import Home from './components/routes/home.tsx' import { ThemeProvider } from './components/theme-provider.tsx' -import { $authenticated, $systems, pb, $publicKey, $hubVersion } from './lib/stores.ts' +import { + $authenticated, + $systems, + pb, + $publicKey, + $hubVersion, + $copyContent, +} from './lib/stores.ts' import { ModeToggle } from './components/mode-toggle.tsx' import { cn, @@ -42,6 +49,7 @@ import { AddSystemButton } from './components/add-system.tsx' // const ServerDetail = lazy(() => import('./components/routes/system.tsx')) const CommandPalette = lazy(() => import('./components/command-palette.tsx')) const LoginPage = lazy(() => import('./components/login/login.tsx')) +const CopyToClipboardDialog = lazy(() => import('./components/copy-to-clipboard.tsx')) const App = () => { const page = useStore($router) @@ -96,6 +104,7 @@ const App = () => { const Layout = () => { const authenticated = useStore($authenticated) + const copyContent = useStore($copyContent) if (!authenticated) { return ( @@ -187,16 +196,23 @@ const Layout = () => { + {copyContent && ( + + + + )} > ) } ReactDOM.createRoot(document.getElementById('app')!).render( - - - - - - + // strict mode in dev mounts / unmounts components twice + // and breaks the clipboard dialog + // + + + + + // ) diff --git a/beszel/site/tailwind.config.js b/beszel/site/tailwind.config.js index 06e6048..bcdf5b3 100644 --- a/beszel/site/tailwind.config.js +++ b/beszel/site/tailwind.config.js @@ -16,12 +16,12 @@ module.exports = { '2xl': '1400px', }, }, - fontFamily: { - sans: 'Inter, sans-serif', - // body: ['Inter', 'sans-serif'], - // display: ['Inter', 'sans-serif'], - }, extend: { + fontFamily: { + sans: 'Inter, sans-serif', + // body: ['Inter', 'sans-serif'], + // display: ['Inter', 'sans-serif'], + }, screens: { xs: '425px', },
+ Clipboard API requires a secure context (https, localhost, or *.localhost) +