fix: harden profile lifecycle validation and dashboard UX flows

This commit is contained in:
Tam Nhu Tran committed 2026-03-05 18:49:16 +07:00
1 parent 02c81743a0
commit d6fc5dd64c
8 files changed
+184 -36

No files matched your search

-15
View File
@@ -70,9 +70,6 @@ export function useDeleteProfile() {
export function useDiscoverProfileOrphans() {
return useMutation({
mutationFn: () => api.profiles.discoverOrphans(),
onError: (error: Error) => {
toast.error(error.message);
},
});
}
@@ -85,9 +82,6 @@ export function useRegisterProfileOrphans() {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
toast.success('Orphan profiles registration complete');
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
@@ -101,9 +95,6 @@ export function useCopyProfile() {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
toast.success('Profile copied successfully');
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
@@ -111,9 +102,6 @@ export function useExportProfile() {
return useMutation({
mutationFn: ({ name, includeSecrets }: { name: string; includeSecrets?: boolean }) =>
api.profiles.export(name, includeSecrets ?? false),
onError: (error: Error) => {
toast.error(error.message);
},
});
}
@@ -126,8 +114,5 @@ export function useImportProfile() {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
toast.success('Profile imported successfully');
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
+8 -1
View File
@@ -164,6 +164,13 @@ export interface CopyProfileRequest {
force?: boolean;
}
export interface CopyProfileResponse {
success: boolean;
name?: string;
settingsPath?: string;
warnings?: string[];
}
export interface ApiProfileExportBundle {
schemaVersion: 1;
exportedAt: string;
@@ -711,7 +718,7 @@ export const api = {
body: JSON.stringify(data),
}),
copy: (name: string, data: CopyProfileRequest) =>
request(`/profiles/${name}/copy`, {
request<CopyProfileResponse>(`/profiles/${name}/copy`, {
method: 'POST',
body: JSON.stringify(data),
}),
+19 -5
View File
@@ -82,6 +82,8 @@ export function ApiPage() {
onSuccess: () => {
if (selectedProfile === name) {
setSelectedProfile(null);
setEditorHasChanges(false);
setPendingSwitch(null);
}
setDeleteConfirm(null);
},
@@ -135,18 +137,26 @@ export function ApiPage() {
const handleCopySelectedProfile = async () => {
if (!selectedProfileData) return;
const destination = window.prompt(
const destinationInput = window.prompt(
`Copy profile "${selectedProfileData.name}" to new profile name:`,
`${selectedProfileData.name}-copy`
);
if (!destination) return;
if (!destinationInput) return;
const destination = destinationInput.trim();
if (!destination) {
toast.error('Destination profile name cannot be empty');
return;
}
try {
await copyProfileMutation.mutateAsync({
const result = await copyProfileMutation.mutateAsync({
name: selectedProfileData.name,
data: { destination: destination.trim() },
data: { destination },
});
switchToProfile(destination.trim());
switchToProfile(destination);
if (result.warnings && result.warnings.length > 0) {
toast.info(result.warnings.join('\n'));
}
} catch (error) {
toast.error((error as Error).message);
}
@@ -210,6 +220,8 @@ export function ApiPage() {
variant="outline"
onClick={() => void handleDiscoverOrphans()}
disabled={discoverOrphansMutation.isPending || registerOrphansMutation.isPending}
aria-label="Discover orphan profiles"
title="Discover orphan profiles"
>
<RefreshCw
className={`w-4 h-4 ${discoverOrphansMutation.isPending ? 'animate-spin' : ''}`}
@@ -220,6 +232,8 @@ export function ApiPage() {
variant="outline"
onClick={handleImportClick}
disabled={importProfileMutation.isPending}
aria-label="Import profile bundle"
title="Import profile bundle"
>
<Upload className="w-4 h-4" />
</Button>