mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
fix: prevent async confirmation races in API key deletion
This commit is contained in:
@@ -32,6 +32,9 @@ export function ConfirmationModal() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,7 @@ export function ConfirmationModal() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isOpen} onClose={handleCancel} title={title}>
|
<Modal open={isOpen} onClose={handleCancel} title={title} closeDisabled={isLoading}>
|
||||||
<p style={{ margin: '1rem 0' }}>{message}</p>
|
<p style={{ margin: '1rem 0' }}>{message}</p>
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
|
||||||
<Button variant="ghost" onClick={handleCancel} disabled={isLoading}>
|
<Button variant="ghost" onClick={handleCancel} disabled={isLoading}>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface ModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
|
closeDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLOSE_ANIMATION_DURATION = 350;
|
const CLOSE_ANIMATION_DURATION = 350;
|
||||||
@@ -32,7 +33,15 @@ const unlockScroll = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren<ModalProps>) {
|
export function Modal({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
footer,
|
||||||
|
width = 520,
|
||||||
|
closeDisabled = false,
|
||||||
|
children
|
||||||
|
}: PropsWithChildren<ModalProps>) {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -106,7 +115,13 @@ export function Modal({ open, title, onClose, footer, width = 520, children }: P
|
|||||||
const modalContent = (
|
const modalContent = (
|
||||||
<div className={overlayClass}>
|
<div className={overlayClass}>
|
||||||
<div className={modalClass} style={{ width }} role="dialog" aria-modal="true">
|
<div className={modalClass} style={{ width }} role="dialog" aria-modal="true">
|
||||||
<button className="modal-close-floating" onClick={handleClose} aria-label="Close">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="modal-close-floating"
|
||||||
|
onClick={closeDisabled ? undefined : handleClose}
|
||||||
|
aria-label="Close"
|
||||||
|
disabled={closeDisabled}
|
||||||
|
>
|
||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</button>
|
</button>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
|
|||||||
@@ -115,14 +115,32 @@ export function ApiKeysPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
|
const apiKeyToDelete = apiKeys[index];
|
||||||
|
if (!apiKeyToDelete) {
|
||||||
|
showNotification(t('notification.delete_failed'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showConfirmation({
|
showConfirmation({
|
||||||
title: t('common.delete'),
|
title: t('common.delete'),
|
||||||
message: t('api_keys.delete_confirm'),
|
message: t('api_keys.delete_confirm'),
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
|
const latestKeys = useConfigStore.getState().config?.apiKeys;
|
||||||
|
const currentKeys = Array.isArray(latestKeys) ? latestKeys : [];
|
||||||
|
const deleteIndex =
|
||||||
|
currentKeys[index] === apiKeyToDelete
|
||||||
|
? index
|
||||||
|
: currentKeys.findIndex((key) => key === apiKeyToDelete);
|
||||||
|
|
||||||
|
if (deleteIndex < 0) {
|
||||||
|
showNotification(t('notification.delete_failed'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiKeysApi.delete(index);
|
await apiKeysApi.delete(deleteIndex);
|
||||||
const nextKeys = apiKeys.filter((_, idx) => idx !== index);
|
const nextKeys = currentKeys.filter((_, idx) => idx !== deleteIndex);
|
||||||
setApiKeys(nextKeys);
|
setApiKeys(nextKeys);
|
||||||
updateConfigValue('api-keys', nextKeys);
|
updateConfigValue('api-keys', nextKeys);
|
||||||
clearCache('api-keys');
|
clearCache('api-keys');
|
||||||
|
|||||||
@@ -453,6 +453,18 @@ textarea {
|
|||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled:hover {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
|||||||
Reference in New Issue
Block a user