Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 10 additions & 33 deletions plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const mockGetManagedNamespaces = vi.hoisted(() => vi.fn());
const mockGetManagedNamespaceDetails = vi.hoisted(() => vi.fn());
const mockUpdateManagedNamespace = vi.hoisted(() => vi.fn());
const mockT = vi.hoisted(() => (key: string) => key);
const mockClusterAction = vi.hoisted(() =>
vi.fn(async (action: () => Promise<void>) => {
try {
await action();
} catch {}
})
);

vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
K8s: {
Expand All @@ -21,6 +28,7 @@ vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
},
},
useTranslation: () => ({ t: mockT }),
clusterAction: mockClusterAction,
}));

vi.mock('../../../utils/azure/az-namespaces', () => ({
Expand Down Expand Up @@ -331,7 +339,7 @@ describe('useInfoTab', () => {
expect(result.current.hasChanges).toBe(false);
});

test('handleSave sets error and leaves updating false when updateManagedNamespace throws', async () => {
test('handleSave leaves updating as false when updateManagedNamespace throws', async () => {
mockUpdateManagedNamespace.mockRejectedValue(new Error('update failed'));

const { result } = renderHook(() => useInfoTab(defaultProject));
Expand All @@ -346,38 +354,7 @@ describe('useInfoTab', () => {
await result.current.handleSave();
});

expect(result.current.error).toBe('Failed to update managed namespace');
expect(result.current.updating).toBe(false);
});

test('handleSave clears a previous error on success', async () => {
mockUpdateManagedNamespace.mockRejectedValueOnce(new Error('update failed'));
mockUpdateManagedNamespace.mockResolvedValueOnce(undefined);

const { result } = renderHook(() => useInfoTab(defaultProject));

await waitFor(() => expect(result.current.loading).toBe(false));

act(() => {
result.current.handleFormDataChange({ ingress: 'DenyAll' });
});

await act(async () => {
await result.current.handleSave();
});

expect(result.current.error).toBe('Failed to update managed namespace');

act(() => {
result.current.handleFormDataChange({ ingress: 'AllowAll' });
});

await act(async () => {
await result.current.handleSave();
});

expect(result.current.error).toBeNull();
expect(mockUpdateManagedNamespace).toHaveBeenCalledTimes(2);
await waitFor(() => expect(result.current.updating).toBe(false));
});

test('handleSave is a no-op when resourceGroup label is absent', async () => {
Expand Down
61 changes: 35 additions & 26 deletions plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache 2.0.

import { K8s, useTranslation } from '@kinvolk/headlamp-plugin/lib';
import { clusterAction, K8s, useTranslation } from '@kinvolk/headlamp-plugin/lib';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
getManagedNamespaceDetails,
Expand Down Expand Up @@ -81,7 +81,7 @@ export interface UseInfoTabResult {
/** Updates form fields and re-validates. */
handleFormDataChange: (updates: Partial<FormData>) => void;
/** Persists the current form data to the managed namespace. */
handleSave: () => Promise<void>;
handleSave: () => void;
}

/**
Expand Down Expand Up @@ -253,32 +253,41 @@ export const useInfoTab = (project: {
return keys.some(k => formData[k] !== baselineFormData[k]);
}, [baselineFormData, formData]);

const handleSave = useCallback(async () => {
const handleSave = useCallback(() => {
if (!resourceGroup || !clusterName || !projectId) return;

try {
setUpdating(true);
await updateManagedNamespace({
clusterName,
resourceGroup,
namespaceName: projectId,
ingressPolicy: formData.ingress,
egressPolicy: formData.egress,
cpuRequest: formData.cpuRequest,
cpuLimit: formData.cpuLimit,
memoryRequest: formData.memoryRequest,
memoryLimit: formData.memoryLimit,
noWait: false,
});
setBaselineFormData(formData);
setError(null);
} catch (e) {
console.error('Failed to update managed namespace', e);
setError(t('Failed to update managed namespace'));
} finally {
setUpdating(false);
}
}, [resourceGroup, clusterName, projectId, formData]);
setUpdating(true);
clusterAction(
async () => {
try {
await updateManagedNamespace({
clusterName,
resourceGroup,
namespaceName: projectId,
ingressPolicy: formData.ingress,
egressPolicy: formData.egress,
cpuRequest: formData.cpuRequest,
cpuLimit: formData.cpuLimit,
memoryRequest: formData.memoryRequest,
memoryLimit: formData.memoryLimit,
subscriptionId: subscription,
noWait: true,
});
setBaselineFormData(formData);
setError(null);
} finally {
setUpdating(false);
}
},
{
startMessage: t('Updating namespace {{ name }}…', { name: projectId }),
cancelledMessage: t('Cancelled update of namespace {{ name }}.', { name: projectId }),
successMessage: t('Updated namespace {{ name }}.', { name: projectId }),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'll need to run npm run i18n:collect at the top-level to update these translations

errorMessage: t('Failed to update namespace {{ name }}.', { name: projectId }),
startOptions: { autoHideDuration: null },
}
);
}, [resourceGroup, clusterName, projectId, subscription, formData, t]);

return {
loading,
Expand Down
18 changes: 18 additions & 0 deletions plugins/aks-desktop/src/utils/azure/az-namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,24 @@ export async function updateManagedNamespace(options: {
maybePush('--ingress-policy', ingressPolicy as string | undefined);
maybePush('--egress-policy', egressPolicy as string | undefined);

// Re-pass existing labels to pass into az namespace update command
// so the ARM PUT doesn't wipe them.
try {
const current = await getManagedNamespaceDetails({
clusterName,
resourceGroup,
namespaceName,
subscriptionId,
});
const labels = current?.properties?.labels ?? current?.labels;
const entries = labels && typeof labels === 'object' ? Object.entries(labels) : [];
if (entries.length > 0) {
args.push('--labels', ...entries.map(([k, v]) => `${k}=${v}`));
}
} catch {
// Proceed without labels if the fetch fails.
Comment on lines +235 to +236
}

if (subscriptionId) {
args.push('--subscription', subscriptionId);
}
Expand Down
Loading