Skip to content

Commit c8a42e4

Browse files
authored
fix(web): support clipboard copy in HTTP environments (#624)
1 parent 8f763a7 commit c8a42e4

13 files changed

Lines changed: 835 additions & 14 deletions

File tree

web/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dev": "vite",
88
"build": "tsc -b && vite build",
99
"lint": "eslint .",
10+
"test": "vitest run",
1011
"preview": "vite preview"
1112
},
1213
"dependencies": {
@@ -69,10 +70,12 @@
6970
"eslint-plugin-react-hooks": "^5.2.0",
7071
"eslint-plugin-react-refresh": "^0.4.19",
7172
"globals": "^16.0.0",
73+
"jsdom": "^26.1.0",
7274
"tw-animate-css": "^1.2.8",
7375
"typescript": "~5.7.2",
7476
"typescript-eslint": "^8.26.1",
75-
"vite": "^6.3.1"
77+
"vite": "^6.3.1",
78+
"vitest": "^3.2.4"
7679
},
7780
"packageManager": "pnpm@10.32.0+sha512.9b2634bb3fed5601c33633f2d92593f506270a3963b8c51d2b2d6a828da615ce4e9deebef9614ccebbc13ac8d3c0f9c9ccceb583c69c8578436fa477dbb20d70"
7881
}

web/pnpm-lock.yaml

Lines changed: 650 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/components/common/CopyButton.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react'
22
import { Button } from '@/components/ui/button'
33
import { Check, Copy } from 'lucide-react'
4+
import { writeTextToClipboard } from '@/lib/clipboard'
45

56
interface CopyButtonProps {
67
text: string
@@ -11,9 +12,10 @@ export const CopyButton = ({ text, className }: CopyButtonProps) => {
1112
const [copied, setCopied] = useState(false)
1213

1314
const copyToClipboard = () => {
14-
navigator.clipboard.writeText(text)
15-
setCopied(true)
16-
setTimeout(() => setCopied(false), 2000)
15+
writeTextToClipboard(text).then(() => {
16+
setCopied(true)
17+
setTimeout(() => setCopied(false), 2000)
18+
}).catch(() => {})
1719
}
1820

1921
return (
@@ -26,4 +28,4 @@ export const CopyButton = ({ text, className }: CopyButtonProps) => {
2628
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
2729
</Button>
2830
)
29-
}
31+
}

web/src/feature/group/components/CreateGroupTokenDialog.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Loader2 } from 'lucide-react'
2323
import { useMutation, useQueryClient } from '@tanstack/react-query'
2424
import { tokenApi } from '@/api/token'
2525
import { toast } from 'sonner'
26+
import { writeTextToClipboard } from '@/lib/clipboard'
2627

2728
interface CreateGroupTokenDialogProps {
2829
open: boolean
@@ -59,8 +60,10 @@ export function CreateGroupTokenDialog({
5960
queryClient.invalidateQueries({ queryKey: ['groups'] })
6061
toast.success(t('common.success'))
6162
if (data?.key) {
62-
navigator.clipboard.writeText(data.key).then(() => {
63+
writeTextToClipboard(data.key).then(() => {
6364
toast.success(t('common.copied'))
65+
}).catch(() => {
66+
toast.error(t('common.copyFailed'))
6467
})
6568
}
6669
resetForm()

web/src/feature/group/components/GroupModelsTab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { PriceDisplay } from '@/components/price/PriceDisplay'
2525
import { toast } from 'sonner'
2626
import { Copy, Search } from 'lucide-react'
2727
import { useGroupModelMetrics } from '@/feature/monitor/runtime-hooks'
28+
import { writeTextToClipboard } from '@/lib/clipboard'
2829

2930
interface GroupModelsTabProps {
3031
groupId: string
@@ -79,7 +80,7 @@ export function GroupModelsTab({ groupId }: GroupModelsTabProps) {
7980
}, [models, searchKeyword, ownerFilter])
8081
const { data: runtimeMetrics } = useGroupModelMetrics(groupId, !!groupId && filteredModels.length > 0)
8182
const copyToClipboard = (text: string) => {
82-
navigator.clipboard.writeText(text).then(() => {
83+
writeTextToClipboard(text).then(() => {
8384
toast.success(t('common.copied'))
8485
}).catch(() => {
8586
toast.error(t('common.copyFailed'))

web/src/feature/group/components/GroupTokensTab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { cn } from '@/lib/utils'
2929
import { toast } from 'sonner'
3030
import { useGroupTokenMetrics } from '@/feature/monitor/runtime-hooks'
3131
import { format } from 'date-fns'
32+
import { writeTextToClipboard } from '@/lib/clipboard'
3233

3334
// Mask API key - show prefix and last 4 chars
3435
const maskApiKey = (key: string): string => {
@@ -168,7 +169,7 @@ export function GroupTokensTab({ groupId, onNavigateDashboard }: GroupTokensTabP
168169
}
169170

170171
const copyToClipboard = (text: string) => {
171-
navigator.clipboard.writeText(text).then(() => {
172+
writeTextToClipboard(text).then(() => {
172173
toast.success(t('common.copied'))
173174
}).catch(() => {
174175
toast.error(t('common.copyFailed'))

web/src/feature/log/components/ExpandedLogContent.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ChannelDialog } from '@/feature/channel/components/ChannelDialog'
1212
import type { Channel } from '@/types/channel'
1313
import { toast } from 'sonner'
1414
import { openResourceDialog, showDeletedResourceToast } from '@/utils/resource-dialog'
15+
import { writeTextToClipboard } from '@/lib/clipboard'
1516

1617
// Format price with unit
1718
const formatPrice = (price: number, unit: number): string => {
@@ -99,7 +100,7 @@ export const ExpandedLogContent = ({ log }: { log: LogRecord }) => {
99100

100101
const copyToClipboard = (text?: string) => {
101102
if (!text) return
102-
navigator.clipboard.writeText(text).then(() => {
103+
writeTextToClipboard(text).then(() => {
103104
toast.success(t('common.copied'))
104105
}).catch(() => {
105106
toast.error(t('common.copyFailed'))

web/src/feature/log/components/LogTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button'
1313
import { ExpandedLogContent } from './ExpandedLogContent'
1414
import { toast } from 'sonner'
1515
import type { LogRecord } from '@/types/log'
16+
import { writeTextToClipboard } from '@/lib/clipboard'
1617

1718
const columnHelper = createColumnHelper<LogRecord>()
1819

@@ -55,7 +56,7 @@ export function LogTable({
5556
}
5657

5758
const copyToClipboard = useCallback((text: string) => {
58-
navigator.clipboard.writeText(text).then(() => {
59+
writeTextToClipboard(text).then(() => {
5960
toast.success(t('common.copied'))
6061
}).catch(() => {
6162
toast.error(t('common.copyFailed'))

web/src/feature/model/components/ModelTable.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { toast } from "sonner";
6363
import { useQueryClient } from "@tanstack/react-query";
6464
import { openResourceDialog, showDeletedResourceToast } from "@/utils/resource-dialog";
6565
import { getChannelModelMetric } from "@/utils/runtime-metrics";
66+
import { writeTextToClipboard } from "@/lib/clipboard";
6667

6768
export function ModelTable() {
6869
const { t } = useTranslation();
@@ -213,8 +214,10 @@ export function ModelTable() {
213214
<div
214215
className="font-medium cursor-pointer hover:text-primary transition-colors"
215216
onClick={() => {
216-
navigator.clipboard.writeText(row.original.model).then(() => {
217+
writeTextToClipboard(row.original.model).then(() => {
217218
toast.success(t("common.copied"));
219+
}).catch(() => {
220+
toast.error(t("common.copyFailed"));
218221
});
219222
}}
220223
>

web/src/feature/model/components/api-doc/ApiDoc.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
1010
import CodeBlock from './CodeHight'
1111
import { toast } from 'sonner'
1212
import { TFunction } from 'i18next'
13+
import { writeTextToClipboard } from '@/lib/clipboard'
1314

1415
interface ApiDocContent {
1516
title: string
@@ -1002,7 +1003,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
10021003

10031004
<Button
10041005
onClick={() => {
1005-
navigator.clipboard.writeText(apiDoc.requestExample).then(
1006+
writeTextToClipboard(apiDoc.requestExample).then(
10061007
() => {
10071008
toast.success(t('common.copied'))
10081009
},
@@ -1124,7 +1125,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
11241125

11251126
<Button
11261127
onClick={() => {
1127-
navigator.clipboard.writeText(apiDoc.responseExample).then(
1128+
writeTextToClipboard(apiDoc.responseExample).then(
11281129
() => {
11291130
toast.success(t('common.copied'))
11301131
},

0 commit comments

Comments
 (0)