Files
bambuddy/frontend/src/components/CopyButton.tsx
T
maziggy 689f5276e4 Show the compose directory in the Docker update command (#2664)
The printed command only works from the directory holding the compose
file, which is the thing the user came to the page not knowing. Adds a
copy button, a saved Compose directory setting, BAMBUDDY_COMPOSE_DIR,
and best-effort detection from a bind mount's host path.

Compose records the directory on every container it creates, but reading
that label needs the Docker socket mounted in — root-equivalent access
for a convenience string. The mountinfo guess is a prefill only: its root
field is relative to the mounted device, so a compose dir on its own
mount loses that prefix, and nothing in the container can detect it.

The field is restricted to path characters. It is the one setting whose
purpose is to be pasted into a root shell, so "/opt/bambuddy; rm -rf /"
would otherwise render as a plausible update command.
2026-08-03 15:12:02 +02:00

73 lines
2.4 KiB
TypeScript

import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Copy, Check } from 'lucide-react';
interface CopyButtonProps {
value: string;
/** i18n key for the resting tooltip. */
titleKey?: string;
/** i18n key for the tooltip while the tick is showing. */
copiedTitleKey?: string;
className?: string;
iconClassName?: string;
}
/**
* Copy-to-clipboard button with the plain-HTTP fallback (#1174).
*
* Lifted out of PrinterInfoModal when the Docker update instructions needed
* the same control (#2664). The fallback is the whole reason this is shared
* rather than re-written per call site: navigator.clipboard is gated behind
* the secure-context requirement, so on a LAN install reached over plain HTTP
* — which is most Bambuddy installs — the API is simply undefined, and a
* naive implementation swallows the failure with no tick and nothing copied.
*/
export function CopyButton({
value,
titleKey = 'printers.copyToClipboard',
copiedTitleKey = 'printers.copied',
className = 'ml-2 p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors',
iconClassName = 'w-3.5 h-3.5',
}: CopyButtonProps) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(value);
} else {
// Legacy execCommand path via an off-screen textarea, matching the
// pattern used by CameraTokensPage's plaintext-token modal.
const ta = document.createElement('textarea');
ta.value = value;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
try {
ta.select();
const ok = document.execCommand('copy');
if (!ok) return;
} finally {
document.body.removeChild(ta);
}
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Both paths failed (no clipboard API, no execCommand). Leave the icon
// unchanged so the user knows nothing was copied.
}
};
return (
<button
onClick={handleCopy}
className={className}
title={copied ? t(copiedTitleKey) : t(titleKey)}
>
{copied ? <Check className={`${iconClassName} text-bambu-green`} /> : <Copy className={iconClassName} />}
</button>
);
}