Skip to content

Commit

Permalink
feat(subnets): Edit reserved IPs MAASENG-2976 & MAASENG-2984 (#5435)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndv99 authored May 16, 2024
1 parent 9774222 commit a58fbfd
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 41 deletions.
50 changes: 50 additions & 0 deletions src/app/store/reservedip/reducers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,56 @@ describe("create reducers", () => {
});
});

describe("update reducers", () => {
it("should correctly reduce updateStart", () => {
const initialState = factory.reservedIpState({ saving: false });
expect(reducers(initialState, actions.updateStart())).toEqual(
factory.reservedIpState({
saving: true,
})
);
});

it("should correctly reduce updateSuccess", () => {
const reservedIp = factory.reservedIp();
const initialState = factory.reservedIpState({
items: [reservedIp],
saving: true,
saved: false,
});
expect(reducers(initialState, actions.updateSuccess(reservedIp))).toEqual(
factory.reservedIpState({
saving: false,
saved: true,
items: [reservedIp],
})
);
});

it("should correctly reduce updateError", () => {
const initialState = factory.reservedIpState({
saving: true,
saved: false,
});
expect(reducers(initialState, actions.updateError("Error"))).toEqual(
factory.reservedIpState({
saving: false,
errors: "Error",
})
);
});

it("should correctly reduce updateNotify", () => {
const items = [factory.reservedIp(), factory.reservedIp()];
const initialState = factory.reservedIpState({
items,
});
expect(
reducers(initialState, actions.updateNotify(items[1])).items
).toEqual(items);
});
});

describe("delete reducers", () => {
it("should correctly reduce deleteStart", () => {
const initialState = factory.reservedIpState({ saving: false });
Expand Down
14 changes: 14 additions & 0 deletions src/app/store/reservedip/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ const reservedIpSlice = createSlice({
}
},
},
updateSuccess: (
state: ReservedIpState,
action: PayloadAction<ReservedIp>
) => {
commonReducers.updateSuccess(state);
const item = action.payload;
const index = (state.items as ReservedIp[]).findIndex(
(draftItem: ReservedIp) =>
draftItem[ReservedIpMeta.PK] === item[ReservedIpMeta.PK]
);
if (index !== -1) {
state.items[index] = item;
}
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ afterAll(() => {

it("displays an error if an invalid IP address is entered", async () => {
renderWithBrowserRouter(
<ReserveDHCPLease setSidePanelContent={vi.fn()} subnetId={1} />,
<ReserveDHCPLease
setSidePanelContent={vi.fn()}
subnetId={state.subnet.items[0].id}
/>,
{ state }
);

Expand All @@ -55,7 +58,10 @@ it("displays an error if an invalid IP address is entered", async () => {
it("displays an error if an out-of-range IP address is entered", async () => {
state.subnet.items = [factory.subnet({ id: 1, cidr: "10.0.0.0/25" })];
renderWithBrowserRouter(
<ReserveDHCPLease setSidePanelContent={vi.fn()} subnetId={1} />,
<ReserveDHCPLease
setSidePanelContent={vi.fn()}
subnetId={state.subnet.items[0].id}
/>,
{ state }
);

Expand All @@ -73,7 +79,10 @@ it("displays an error if an out-of-range IP address is entered", async () => {
it("closes the side panel when the cancel button is clicked", async () => {
const setSidePanelContent = vi.fn();
renderWithBrowserRouter(
<ReserveDHCPLease setSidePanelContent={setSidePanelContent} subnetId={1} />,
<ReserveDHCPLease
setSidePanelContent={setSidePanelContent}
subnetId={state.subnet.items[0].id}
/>,
{ state }
);

Expand All @@ -85,7 +94,10 @@ it("closes the side panel when the cancel button is clicked", async () => {
it("dispatches an action to create a reserved IP", async () => {
const store = mockStore(state);
renderWithBrowserRouter(
<ReserveDHCPLease setSidePanelContent={vi.fn()} subnetId={1} />,
<ReserveDHCPLease
setSidePanelContent={vi.fn()}
subnetId={state.subnet.items[0].id}
/>,
{ store }
);

Expand Down Expand Up @@ -126,3 +138,88 @@ it("dispatches an action to create a reserved IP", async () => {
type: "reservedip/create",
});
});

it("pre-fills the form if a reserved IP's ID is present", async () => {
const reservedIp = factory.reservedIp({
id: 1,
ip: "10.0.0.2",
mac_address: "FF:FF:FF:FF:FF:FF",
comment: "bla bla bla",
});
state.reservedip = factory.reservedIpState({
loading: false,
loaded: true,
items: [reservedIp],
});

renderWithBrowserRouter(
<ReserveDHCPLease
reservedIpId={reservedIp.id}
setSidePanelContent={vi.fn()}
subnetId={state.subnet.items[0].id}
/>,
{ state }
);

expect(screen.getByRole("textbox", { name: "IP address" })).toHaveValue("2");
expect(screen.getByRole("textbox", { name: "MAC address" })).toHaveValue(
reservedIp.mac_address
);
expect(screen.getByRole("textbox", { name: "Comment" })).toHaveValue(
reservedIp.comment
);
});

it("dispatches an action to update a reserved IP", async () => {
const reservedIp = factory.reservedIp({
id: 1,
ip: "10.0.0.69",
mac_address: "FF:FF:FF:FF:FF:FF",
comment: "bla bla bla",
});
state.reservedip = factory.reservedIpState({
loading: false,
loaded: true,
items: [reservedIp],
});

const store = mockStore(state);
renderWithBrowserRouter(
<ReserveDHCPLease
reservedIpId={reservedIp.id}
setSidePanelContent={vi.fn()}
subnetId={state.subnet.items[0].id}
/>,
{ store }
);

await userEvent.clear(screen.getByRole("textbox", { name: "Comment" }));

await userEvent.type(
screen.getByRole("textbox", { name: "Comment" }),
"something imaginative and funny"
);

await userEvent.click(
screen.getByRole("button", { name: "Update static DHCP lease" })
);

expect(
store.getActions().find((action) => action.type === "reservedip/update")
).toEqual({
meta: {
method: "update",
model: "reservedip",
},
payload: {
params: {
subnet: 1,
id: reservedIp.id,
ip: "10.0.0.69",
mac_address: "FF:FF:FF:FF:FF:FF",
comment: "something imaginative and funny",
},
},
type: "reservedip/update",
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,63 @@ import {
isIpInSubnet,
} from "@/app/utils/subnetIpRange";

type Props = Pick<SubnetActionProps, "subnetId" | "setSidePanelContent">;
const MAX_COMMENT_LENGTH = 255;

type Props = Pick<
SubnetActionProps,
"subnetId" | "setSidePanelContent" | "reservedIpId"
>;

type FormValues = {
ip_address: string;
mac_address: string;
comment: string;
};

const ReserveDHCPLease = ({ subnetId, setSidePanelContent }: Props) => {
const ReserveDHCPLease = ({
subnetId,
setSidePanelContent,
reservedIpId,
}: Props) => {
const subnet = useSelector((state: RootState) =>
subnetSelectors.getById(state, subnetId)
);
const reservedIp = useSelector((state: RootState) =>
reservedIpSelectors.getById(state, reservedIpId)
);
const subnetLoading = useSelector(subnetSelectors.loading);
const reservedIpLoading = useSelector(reservedIpSelectors.loading);
const errors = useSelector(reservedIpSelectors.errors);
const saving = useSelector(reservedIpSelectors.saving);
const saved = useSelector(reservedIpSelectors.saved);
const dispatch = useDispatch();
const cleanup = useCallback(() => reservedIpActions.cleanup(), []);

const loading = subnetLoading || reservedIpLoading;
const isEditing = !!reservedIpId;

const getInitialValues = () => {
if (reservedIp && subnet) {
const [startIp, endIp] = getIpRangeFromCidr(subnet.cidr);
const [immutableOctets, _] = getImmutableAndEditableOctets(
startIp,
endIp
);
return {
ip_address: reservedIp.ip.replace(`${immutableOctets}.`, ""),
mac_address: reservedIp.mac_address || "",
comment: reservedIp.comment || "",
};
} else {
return {
ip_address: "",
mac_address: "",
comment: "",
};
}
};

const dispatch = useDispatch();
const cleanup = useCallback(() => reservedIpActions.cleanup(), []);
const initialValues = getInitialValues();

const onClose = () => setSidePanelContent(null);

Expand Down Expand Up @@ -96,50 +131,71 @@ const ReserveDHCPLease = ({ subnetId, setSidePanelContent }: Props) => {

const handleSubmit = (values: FormValues) => {
dispatch(cleanup());

dispatch(
reservedIpActions.create({
comment: values.comment,
ip: `${immutableOctets}.${values.ip_address}`,
mac_address: values.mac_address,
subnet: subnetId,
})
);
if (isEditing) {
dispatch(
reservedIpActions.update({
comment: values.comment,
ip: `${immutableOctets}.${values.ip_address}`,
mac_address: values.mac_address,
subnet: subnetId,
id: reservedIpId,
})
);
} else {
dispatch(
reservedIpActions.create({
comment: values.comment,
ip: `${immutableOctets}.${values.ip_address}`,
mac_address: values.mac_address,
subnet: subnetId,
})
);
}
};

return (
<FormikForm<FormValues>
aria-label="Reserve static DHCP lease"
aria-label={
isEditing ? "Edit static DHCP lease" : "Reserve static DHCP lease"
}
cleanup={cleanup}
enableReinitialize
errors={errors}
initialValues={{
ip_address: "",
mac_address: "",
comment: "",
}}
initialValues={initialValues}
onCancel={onClose}
onSubmit={handleSubmit}
onSuccess={onClose}
resetOnSave
saved={saved}
saving={saving}
submitLabel="Reserve static DHCP lease"
submitLabel={
isEditing ? "Update static DHCP lease" : "Reserve static DHCP lease"
}
validationSchema={ReserveDHCPLeaseSchema}
>
<FormikField
cidr={subnet.cidr}
component={PrefixedIpInput}
label="IP address"
name="ip_address"
required
/>
<MacAddressField label="MAC address" name="mac_address" />
<FormikField
label="Comment"
name="comment"
placeholder="Static DHCP lease purpose"
type="text"
/>
{({ values }: { values: FormValues }) => (
<>
<FormikField
cidr={subnet.cidr}
component={PrefixedIpInput}
label="IP address"
name="ip_address"
required
/>
<MacAddressField label="MAC address" name="mac_address" />
<FormikField
className="u-margin-bottom--x-small"
label="Comment"
maxLength={MAX_COMMENT_LENGTH}
name="comment"
placeholder="Static DHCP lease purpose"
type="text"
/>
<small className="u-flex--end">
{values.comment.length}/{MAX_COMMENT_LENGTH}
</small>
</>
)}
</FormikForm>
);
};
Expand Down
Loading

0 comments on commit a58fbfd

Please sign in to comment.