Skip to content

Commit 3c5e1ed

Browse files
refactor(ui-react): use per-key custom_field endpoints
Replaces useUpdateDeviceCustomFields with useSetDeviceCustomField and useDeleteDeviceCustomField. CustomFieldsSection now PUTs a single key on add and DELETEs a single key on remove, dropping the bulk map replace that forced sending name in the body. Gates the section on the new device:customField:update permission.
1 parent 7237db0 commit 3c5e1ed

4 files changed

Lines changed: 35 additions & 25 deletions

File tree

ui-react/apps/console/src/hooks/useDeviceMutations.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
deleteDeviceMutation,
77
updateDeviceMutation,
88
pullTagFromDeviceMutation,
9+
setDeviceCustomFieldMutation,
10+
deleteDeviceCustomFieldMutation,
911
} from "../client/@tanstack/react-query.gen";
1012
import { createTag, pushTagToDevice } from "../client";
1113
import { useInvalidateByIds } from "./useInvalidateQueries";
@@ -42,10 +44,18 @@ export function useRenameDevice() {
4244
});
4345
}
4446

45-
export function useUpdateDeviceCustomFields() {
47+
export function useSetDeviceCustomField() {
4648
const invalidate = useInvalidateByIds("getDevices", "getDevice");
4749
return useMutation({
48-
...updateDeviceMutation(),
50+
...setDeviceCustomFieldMutation(),
51+
onSuccess: invalidate,
52+
});
53+
}
54+
55+
export function useDeleteDeviceCustomField() {
56+
const invalidate = useInvalidateByIds("getDevices", "getDevice");
57+
return useMutation({
58+
...deleteDeviceCustomFieldMutation(),
4959
onSuccess: invalidate,
5060
});
5161
}

ui-react/apps/console/src/pages/DeviceDetails.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
useAddDeviceTag,
2626
useRemoveDeviceTag,
2727
useRemoveDevice,
28-
useUpdateDeviceCustomFields,
28+
useSetDeviceCustomField,
29+
useDeleteDeviceCustomField,
2930
} from "../hooks/useDeviceMutations";
3031
import { useNamespace } from "../hooks/useNamespaces";
3132
import { useAuthStore } from "../stores/authStore";
@@ -283,8 +284,9 @@ function CustomFieldsSection({
283284
uid: string;
284285
customFields: Record<string, string>;
285286
}) {
286-
const mutation = useUpdateDeviceCustomFields();
287-
const canEdit = useHasPermission("device:rename");
287+
const setMutation = useSetDeviceCustomField();
288+
const deleteMutation = useDeleteDeviceCustomField();
289+
const canEdit = useHasPermission("device:customField:update");
288290
const [keyInput, setKeyInput] = useState("");
289291
const [valueInput, setValueInput] = useState("");
290292
const [adding, setAdding] = useState(false);
@@ -302,9 +304,9 @@ function CustomFieldsSection({
302304
setError(null);
303305
setAdding(true);
304306
try {
305-
await mutation.mutateAsync({
306-
path: { uid },
307-
body: { name: "", custom_fields: { ...customFields, [key]: value } },
307+
await setMutation.mutateAsync({
308+
path: { uid, key },
309+
body: { value },
308310
});
309311
setKeyInput("");
310312
setValueInput("");
@@ -315,12 +317,9 @@ function CustomFieldsSection({
315317
};
316318

317319
const handleRemove = async (key: string) => {
318-
const updated = { ...customFields };
319-
delete updated[key];
320320
try {
321-
await mutation.mutateAsync({
322-
path: { uid },
323-
body: { name: "", custom_fields: updated },
321+
await deleteMutation.mutateAsync({
322+
path: { uid, key },
324323
});
325324
} catch {
326325
/* invalidation handles UI update */

ui-react/apps/console/src/pages/devices/__tests__/DeviceDetails.test.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ vi.mock("@/hooks/useDevice", () => ({
1111
useDevice: vi.fn(),
1212
}));
1313

14-
const mockUpdateCustomFields = vi.fn();
14+
const mockSetCustomField = vi.fn();
15+
const mockDeleteCustomField = vi.fn();
1516

1617
vi.mock("@/hooks/useDeviceMutations", () => ({
1718
useRenameDevice: () => ({ mutateAsync: vi.fn() }),
1819
useAddDeviceTag: () => ({ mutateAsync: vi.fn() }),
1920
useRemoveDeviceTag: () => ({ mutateAsync: vi.fn() }),
2021
useRemoveDevice: () => ({ mutateAsync: vi.fn() }),
21-
useUpdateDeviceCustomFields: () => ({ mutateAsync: mockUpdateCustomFields }),
22+
useSetDeviceCustomField: () => ({ mutateAsync: mockSetCustomField }),
23+
useDeleteDeviceCustomField: () => ({ mutateAsync: mockDeleteCustomField }),
2224
}));
2325

2426
vi.mock("@/hooks/useNamespaces", () => ({
@@ -122,7 +124,8 @@ function renderPage() {
122124

123125
describe("DeviceDetails", () => {
124126
beforeEach(() => {
125-
mockUpdateCustomFields.mockReset().mockResolvedValue({});
127+
mockSetCustomField.mockReset().mockResolvedValue({});
128+
mockDeleteCustomField.mockReset().mockResolvedValue({});
126129
vi.mocked(useDevice).mockReturnValue({
127130
device: null,
128131
isLoading: false,
@@ -258,11 +261,9 @@ describe("DeviceDetails", () => {
258261
await user.click(xBtn);
259262
await user.click(screen.getByText("Yes"));
260263

261-
expect(mockUpdateCustomFields).toHaveBeenCalledWith(
264+
expect(mockDeleteCustomField).toHaveBeenCalledWith(
262265
expect.objectContaining({
263-
body: expect.objectContaining({
264-
custom_fields: { owner: "team-a" },
265-
}),
266+
path: expect.objectContaining({ uid: "test-uid", key: "env" }),
266267
}),
267268
);
268269
});
@@ -280,11 +281,10 @@ describe("DeviceDetails", () => {
280281
await user.type(screen.getByPlaceholderText("key"), "region");
281282
await user.type(screen.getByPlaceholderText("value"), "us-east{Enter}");
282283

283-
expect(mockUpdateCustomFields).toHaveBeenCalledWith(
284+
expect(mockSetCustomField).toHaveBeenCalledWith(
284285
expect.objectContaining({
285-
body: expect.objectContaining({
286-
custom_fields: { region: "us-east" },
287-
}),
286+
path: expect.objectContaining({ uid: "test-uid", key: "region" }),
287+
body: { value: "us-east" },
288288
}),
289289
);
290290
});
@@ -303,7 +303,7 @@ describe("DeviceDetails", () => {
303303
await user.type(screen.getByPlaceholderText("value"), "staging{Enter}");
304304

305305
expect(screen.getByText("This key already exists.")).toBeInTheDocument();
306-
expect(mockUpdateCustomFields).not.toHaveBeenCalled();
306+
expect(mockSetCustomField).not.toHaveBeenCalled();
307307
});
308308
});
309309
});

ui-react/apps/console/src/utils/permission.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const permissions = {
2525
"device:accept": RoleLevel.OPERATOR,
2626
"device:reject": RoleLevel.OPERATOR,
2727
"device:rename": RoleLevel.OPERATOR,
28+
"device:customField:update": RoleLevel.OPERATOR,
2829
"device:remove": RoleLevel.ADMINISTRATOR,
2930
"device:choose": RoleLevel.OWNER,
3031

0 commit comments

Comments
 (0)