fix: add loading state and error handling to API key creation page
https://sonarly.com/issue/21336?type=bug The "New API Key" page lacks loading feedback during save and silently swallows mutation errors, causing users to rage-click the Save button with no visual response. Fix: Added loading state and error handling to the API key creation page (`SettingsDevelopersApiKeysNew`), matching the exact pattern used in the sibling `SettingsDevelopersApiKeyDetail` component: 1. **Loading state**: Added `useState(false)` for `isLoading`, set to `true` before the async save operation begins, and reset to `false` in a `finally` block. Passed `isLoading` to `SaveAndCancelButtons` which disables the Save button and shows a spinner during the operation. 2. **Error handling**: Wrapped the two sequential mutations (`createApiKey` + `generateApiKeyToken`) in a `try/catch/finally` block. On failure, an error snackbar is displayed via `enqueueErrorSnackBar` — the same hook and pattern used throughout the settings pages. 3. **Silent failure fix**: The `if (!newApiKey) return` silent failure now shows an error snackbar before returning, so users see feedback when the mutation returns no data. These changes prevent rage clicks by: - Showing a loading spinner on the Save button during the async operation - Disabling the button while the operation is in progress (preventing duplicate submissions) - Displaying an error toast if the operation fails (instead of silently doing nothing)
This commit is contained in:
+51
-39
@@ -7,6 +7,7 @@ import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLo
|
||||
import { SettingsDevelopersRoleSelector } from '@/settings/developers/components/SettingsDevelopersRoleSelector';
|
||||
import { EXPIRATION_DATES } from '@/settings/developers/constants/ExpirationDates';
|
||||
import { apiKeyTokenFamilyState } from '@/settings/developers/states/apiKeyTokenFamilyState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
@@ -27,6 +28,8 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
export const SettingsDevelopersApiKeysNew = () => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [generateOneApiKeyToken] = useMutation(GenerateApiKeyTokenDocument);
|
||||
const navigateSettings = useNavigateSettings();
|
||||
const { data: rolesData, loading: rolesLoading } = useQuery(GetRolesDocument);
|
||||
@@ -70,50 +73,58 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formValues.name) return;
|
||||
if (!formValues.name || !formValues.roleId) return;
|
||||
|
||||
const expiresAt = addDays(
|
||||
new Date(),
|
||||
formValues.expirationDate ?? 30,
|
||||
).toISOString();
|
||||
setIsLoading(true);
|
||||
|
||||
const roleIdToUse = formValues.roleId;
|
||||
try {
|
||||
const expiresAt = addDays(
|
||||
new Date(),
|
||||
formValues.expirationDate ?? 30,
|
||||
).toISOString();
|
||||
|
||||
if (!roleIdToUse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: newApiKeyData } = await createApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
name: formValues.name.trim(),
|
||||
expiresAt,
|
||||
roleId: roleIdToUse,
|
||||
const { data: newApiKeyData } = await createApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
name: formValues.name.trim(),
|
||||
expiresAt,
|
||||
roleId: formValues.roleId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newApiKey = newApiKeyData?.createApiKey;
|
||||
|
||||
if (!newApiKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await generateOneApiKeyToken({
|
||||
variables: {
|
||||
apiKeyId: newApiKey.id,
|
||||
expiresAt: expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(tokenData.data?.generateApiKeyToken)) {
|
||||
setApiKeyTokenCallback(
|
||||
newApiKey.id,
|
||||
tokenData.data.generateApiKeyToken.token,
|
||||
);
|
||||
navigateSettings(SettingsPath.ApiKeyDetail, {
|
||||
apiKeyId: newApiKey.id,
|
||||
});
|
||||
|
||||
const newApiKey = newApiKeyData?.createApiKey;
|
||||
|
||||
if (!newApiKey) {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error creating API key`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await generateOneApiKeyToken({
|
||||
variables: {
|
||||
apiKeyId: newApiKey.id,
|
||||
expiresAt: expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(tokenData.data?.generateApiKeyToken)) {
|
||||
setApiKeyTokenCallback(
|
||||
newApiKey.id,
|
||||
tokenData.data.generateApiKeyToken.token,
|
||||
);
|
||||
navigateSettings(SettingsPath.ApiKeyDetail, {
|
||||
apiKeyId: newApiKey.id,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error creating API key`,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,6 +151,7 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
isLoading={isLoading}
|
||||
onCancel={() => {
|
||||
navigateSettings(SettingsPath.ApiWebhooks);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user