Add permission check around clipboard access (#22781)
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
8e649b05c9
commit
82a94cb12b
4 changed files with 85 additions and 22 deletions
|
@ -317,6 +317,7 @@
|
||||||
"copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.",
|
"copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.",
|
||||||
"copySuccess": "Successfully copied to clipboard!",
|
"copySuccess": "Successfully copied to clipboard!",
|
||||||
"clipboardCopyError": "Error copying to clipboard.",
|
"clipboardCopyError": "Error copying to clipboard.",
|
||||||
|
"clipboardCopyDenied": "Your browser is blocking access to the clipboard.",
|
||||||
"copyToClipboard": "Copy to clipboard",
|
"copyToClipboard": "Copy to clipboard",
|
||||||
"clientRegistration": "Client registration",
|
"clientRegistration": "Client registration",
|
||||||
"anonymousAccessPolicies": "Anonymous access polices",
|
"anonymousAccessPolicies": "Anonymous access polices",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
import useSetTimeout from "../../utils/useSetTimeout";
|
import useSetTimeout from "../../utils/useSetTimeout";
|
||||||
|
import useQueryPermission from "../../utils/useQueryPermission";
|
||||||
|
|
||||||
enum CopyState {
|
enum CopyState {
|
||||||
Ready,
|
Ready,
|
||||||
|
@ -27,32 +28,39 @@ export const CopyToClipboardButton = ({
|
||||||
}: CopyToClipboardButtonProps) => {
|
}: CopyToClipboardButtonProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const setTimeout = useSetTimeout();
|
const setTimeout = useSetTimeout();
|
||||||
|
const permission = useQueryPermission("clipboard-write" as PermissionName);
|
||||||
|
const permissionDenied = permission?.state === "denied";
|
||||||
|
const [copyState, setCopyState] = useState(CopyState.Ready);
|
||||||
|
|
||||||
const [copy, setCopy] = useState(CopyState.Ready);
|
// Determine the message to use for the copy button.
|
||||||
|
const copyMessageKey = useMemo(() => {
|
||||||
|
if (permissionDenied) {
|
||||||
|
return "clipboardCopyDenied";
|
||||||
|
}
|
||||||
|
|
||||||
const copyMessage = useMemo(() => {
|
switch (copyState) {
|
||||||
switch (copy) {
|
|
||||||
case CopyState.Ready:
|
case CopyState.Ready:
|
||||||
return t("copyToClipboard");
|
return "copyToClipboard";
|
||||||
case CopyState.Copied:
|
case CopyState.Copied:
|
||||||
return t("copySuccess");
|
return "copySuccess";
|
||||||
case CopyState.Error:
|
case CopyState.Error:
|
||||||
return t("clipboardCopyError");
|
return "clipboardCopyError";
|
||||||
}
|
}
|
||||||
}, [copy]);
|
}, [permissionDenied, copyState]);
|
||||||
|
|
||||||
|
// Reset the message of the copy button after copying to the clipboard.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (copy !== CopyState.Ready) {
|
if (copyState !== CopyState.Ready) {
|
||||||
return setTimeout(() => setCopy(CopyState.Ready), 1000);
|
return setTimeout(() => setCopyState(CopyState.Ready), 1000);
|
||||||
}
|
}
|
||||||
}, [copy]);
|
}, [copyState, setTimeout]);
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopy(CopyState.Copied);
|
setCopyState(CopyState.Copied);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCopy(CopyState.Error);
|
setCopyState(CopyState.Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,7 +73,7 @@ export const CopyToClipboardButton = ({
|
||||||
exitDelay={600}
|
exitDelay={600}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
{copyMessage}
|
{t(copyMessageKey)}
|
||||||
</ClipboardCopyButton>
|
</ClipboardCopyButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
54
js/apps/admin-ui/src/utils/useQueryPermission.ts
Normal file
54
js/apps/admin-ui/src/utils/useQueryPermission.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
/** A 'plain' object version of the permission status. */
|
||||||
|
export type PlainPermissionStatus = {
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: PermissionState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useQueryPermission(
|
||||||
|
name: PermissionName,
|
||||||
|
): PlainPermissionStatus | null {
|
||||||
|
const [status, setStatus] = useState<PermissionStatus | null>(null);
|
||||||
|
const [plainStatus, setPlainStatus] = useState<PlainPermissionStatus | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
function updatePlainStatus(newStatus: PermissionStatus) {
|
||||||
|
setPlainStatus({
|
||||||
|
name: newStatus.name,
|
||||||
|
state: newStatus.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the permission status when the name changes.
|
||||||
|
useEffect(() => {
|
||||||
|
setStatus(null);
|
||||||
|
setPlainStatus(null);
|
||||||
|
|
||||||
|
navigator.permissions.query({ name }).then((newStatus) => {
|
||||||
|
setStatus(newStatus);
|
||||||
|
updatePlainStatus(newStatus);
|
||||||
|
});
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
// Update the 'plain' status when the permission status changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStatusChange() {
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlainStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
status.addEventListener("change", onStatusChange);
|
||||||
|
return () => status.removeEventListener("change", onStatusChange);
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return plainStatus;
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
export default function useSetTimeout() {
|
export default function useSetTimeout() {
|
||||||
const didUnmountRef = useRef(false);
|
const didUnmountRef = useRef(false);
|
||||||
const { current: scheduledTimers } = useRef(new Set<number>());
|
const scheduledTimersRef = useRef(new Set<number>());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
didUnmountRef.current = false;
|
didUnmountRef.current = false;
|
||||||
|
@ -14,27 +14,27 @@ export default function useSetTimeout() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
scheduledTimers.forEach((timer) => clearTimeout(timer));
|
scheduledTimersRef.current.forEach((timer) => clearTimeout(timer));
|
||||||
scheduledTimers.clear();
|
scheduledTimersRef.current.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return function scheduleTimeout(callback: () => void, delay: number) {
|
return useCallback((callback: () => void, delay: number) => {
|
||||||
if (didUnmountRef.current) {
|
if (didUnmountRef.current) {
|
||||||
throw new Error("Can't schedule a timeout on an unmounted component.");
|
throw new Error("Can't schedule a timeout on an unmounted component.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const timer = Number(setTimeout(handleCallback, delay));
|
const timer = Number(setTimeout(handleCallback, delay));
|
||||||
|
|
||||||
scheduledTimers.add(timer);
|
scheduledTimersRef.current.add(timer);
|
||||||
|
|
||||||
function handleCallback() {
|
function handleCallback() {
|
||||||
scheduledTimers.delete(timer);
|
scheduledTimersRef.current.delete(timer);
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
return function cancelTimeout() {
|
return function cancelTimeout() {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
scheduledTimers.delete(timer);
|
scheduledTimersRef.current.delete(timer);
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue