Skip to content

Commit

Permalink
update DHCP add form #1116
Browse files Browse the repository at this point in the history
  • Loading branch information
petermakowski committed Sep 7, 2022
1 parent 29f20c2 commit a698680
Show file tree
Hide file tree
Showing 19 changed files with 421 additions and 70 deletions.
31 changes: 31 additions & 0 deletions cypress/e2e/with-users/settings/dhcp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { generateMAASURL, generateName } from "../../utils";

context("Settings - DHCP Snippets", () => {
beforeEach(() => {
cy.login();
cy.addMachine();
cy.visit(generateMAASURL("/settings/dhcp/add"));
});

it("can add a DHCP snippet to a machine", () => {
const snippetName = generateName("dhcp-snippet");
cy.get("[data-testid='section-header-title']").contains("Settings");
cy.findByLabelText("Snippet name").type(snippetName);
cy.findByLabelText("Type").select("Machine");
cy.findByRole("button", { name: /Choose machine/ }).click();
// ensure the data has loaded
cy.findByRole("grid").should("have.attr", "aria-busy", "false");
cy.get("tbody").within(() => {
cy.findAllByRole("row").first().click();
});
cy.findByLabelText("DHCP snippet").type("ddns-update-style none;");
cy.findByRole("button", { name: "Save snippet" }).click();
// expect to be redirected to the list page
cy.findByLabelText("Search DHCP snippets").type(snippetName);
cy.findByRole("grid").within(() => {
cy.findByText(snippetName).should("be.visible");
cy.findByRole("button", { name: /Delete/ }).click();
cy.get("[data-testid='action-confirm']").click();
});
});
});
13 changes: 13 additions & 0 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import "@testing-library/cypress/add-commands";
import type { Result } from "axe-core";
import { nanoid } from "nanoid";
import { generateMAASURL, generateMac } from "../e2e/utils";
import type { A11yPageContext } from "./e2e";

Cypress.Commands.add("login", (options) => {
Expand Down Expand Up @@ -35,6 +37,17 @@ Cypress.Commands.add("loginNonAdmin", () => {
});
});

Cypress.Commands.add("addMachine", (hostname = `cypress-${nanoid()}`) => {
cy.visit(generateMAASURL("/machines"));
cy.get("[data-testid='add-hardware-dropdown'] button").click();
cy.get(".p-contextual-menu__link").contains("Machine").click();
cy.get("input[name='hostname']").type(hostname);
cy.get("input[name='pxe_mac']").type(generateMac());
cy.get("select[name='power_type']").select("manual").blur();
cy.get("button[type='submit']").click();
cy.get(`[data-testid='message']:contains(${hostname} added successfully.)`);
});

function logViolations(violations: Result[], pageContext: A11yPageContext) {
const divider =
"\n====================================================================================================\n";
Expand Down
1 change: 1 addition & 0 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type A11yPageContext = { url?: string; title?: string };
declare global {
namespace Cypress {
interface Chainable {
addMachine(hostname?: string): void;
login(options?: {
username?: string;
password?: string;
Expand Down
48 changes: 28 additions & 20 deletions src/app/base/components/DhcpFormFields/DhcpFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { useFormikContext } from "formik";
import { useSelector } from "react-redux";

import MachineSelect from "./MachineSelect/MachineSelect";

import type { DHCPFormValues } from "app/base/components/DhcpForm/types";
import FormikField from "app/base/components/FormikField";
import controllerSelectors from "app/store/controller/selectors";
Expand Down Expand Up @@ -73,12 +75,8 @@ export const DhcpFormFields = ({ editing }: Props): JSX.Element => {
const controllerLoaded = useSelector(controllerSelectors.loaded);
const deviceLoading = useSelector(deviceSelectors.loading);
const deviceLoaded = useSelector(deviceSelectors.loaded);
const machineLoading = useSelector(machineSelectors.loading);
const machineLoaded = useSelector(machineSelectors.loaded);
const isLoading =
subnetLoading || controllerLoading || deviceLoading || machineLoading;
const hasLoaded =
subnetLoaded && controllerLoaded && deviceLoaded && machineLoaded;
const isLoading = subnetLoading || controllerLoading || deviceLoading;
const hasLoaded = subnetLoaded && controllerLoaded && deviceLoaded;
const { enabled, type } = formikProps.values;
let models: ModelType[] | null;
switch (type) {
Expand Down Expand Up @@ -134,21 +132,31 @@ export const DhcpFormFields = ({ editing }: Props): JSX.Element => {
{ value: "device", label: "Device" },
]}
/>
{type &&
(isLoading || !hasLoaded ? (
<Spinner aria-label={Labels.LoadingData} text="loading..." />
) : (
<FormikField
component={Select}
label={Labels.Entity}
name="entity"
options={
// This won't need to pass the empty array once this issue is fixed:
// https://github.com/canonical/react-components/issues/570
generateOptions(type, models) || []
{type === "machine" ? (
<MachineSelect
onSelect={(machine) => {
if (machine) {
formikProps.setFieldValue("entity", machine.system_id);
} else {
formikProps.setFieldValue("entity", "");
}
/>
))}
}}
selected={formikProps.values.entity}
/>
) : isLoading || !hasLoaded ? (
<Spinner aria-label={Labels.LoadingData} text="loading..." />
) : (
<FormikField
component={Select}
label={Labels.Entity}
name="entity"
options={
// This won't need to pass the empty array once this issue is fixed:
// https://github.com/canonical/react-components/issues/570
generateOptions(type, models) || []
}
/>
)}
<FormikField
component={Textarea}
grow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import MachineSelect, { Labels } from "./MachineSelect";

import { renderWithMockStore } from "testing/utils";

it("can open select box on click", async () => {
renderWithMockStore(<MachineSelect onSelect={jest.fn()} />);

expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", { name: Labels.ChooseMachine })
);
expect(screen.getByRole("listbox")).toBeInTheDocument();
});

it("sets focus on the input field on open", async () => {
renderWithMockStore(<MachineSelect onSelect={jest.fn()} />);

await userEvent.click(
screen.getByRole("button", { name: Labels.ChooseMachine })
);
expect(
screen.getByPlaceholderText("Search by hostname, system ID or tags")
).toHaveFocus();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useState } from "react";

import { Label, useId } from "@canonical/react-components";
import { useDispatch } from "react-redux";

import SelectButton from "../../SelectButton";

import MachineSelectBox from "./MachineSelectBox";

import type { Machine } from "app/store/machine/types";
import { useFetchMachine } from "app/store/machine/utils/hooks";
import { actions as tagActions } from "app/store/tag";

export enum Labels {
AppliesTo = "Applies to",
Loading = "Loading...",
ChooseMachine = "Choose machine",
}

type Props = {
label?: string;
onSelect: (machine: Machine | null) => void;
selected?: Machine["system_id"] | null;
};

export const MachineSelect = ({
label = Labels.AppliesTo,
onSelect,
selected = null,
}: Props): JSX.Element => {
const dispatch = useDispatch();
const [isOpen, setIsOpen] = useState(false);
const selectId = useId();
const handleSelect = (machine: Machine | null) => {
setIsOpen(false);
onSelect(machine);
};

const { machine: selectedMachine } = useFetchMachine(selected, {
keepPreviousData: true,
});

useEffect(() => {
dispatch(tagActions.fetch());
}, [dispatch]);

return (
<>
<Label id={selectId}>{label}</Label>
<SelectButton
aria-describedby={selectId}
aria-haspopup="listbox"
onClick={() => {
setIsOpen(!isOpen);
if (!isOpen) {
onSelect(null);
}
}}
>
{selectedMachine?.hostname || Labels.ChooseMachine}
</SelectButton>
{isOpen ? <MachineSelectBox onSelect={handleSelect} /> : null}
</>
);
};

export default MachineSelect;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useState } from "react";

import { SearchBox, Pagination } from "@canonical/react-components";

import { MachineSelectTable } from "app/base/components/MachineSelectTable/MachineSelectTable";
import type { Machine } from "app/store/machine/types";
import { FilterGroupKey } from "app/store/machine/types";
import { useFetchMachines } from "app/store/machine/utils/hooks";

const MachineSelectBox = ({
onSelect,
}: {
onSelect: (machine: Machine | null) => void;
}): JSX.Element => {
const pageSize = 15;
const [searchText, setSearchText] = useState("");
const [currentPage, setPage] = useState(1);
const { machines, machineCount, loading } = useFetchMachines({
currentPage,
pageSize,
filters: { [FilterGroupKey.FreeText]: searchText },
});
return (
<div className="source-machine-select" role="listbox">
<SearchBox
autoComplete="off"
autoFocus
externallyControlled
onChange={(searchText: string) => {
setSearchText(searchText);
}}
placeholder="Search by hostname, system ID or tags"
value={searchText}
/>
<div className="source-machine-select__table">
<MachineSelectTable
machines={machines}
machinesLoading={loading}
onMachineClick={(machine) => {
onSelect(machine);
}}
searchText={searchText}
setSearchText={setSearchText}
/>
<Pagination
currentPage={currentPage}
itemsPerPage={pageSize}
paginate={setPage}
totalItems={machineCount || 0}
/>
</div>
</div>
);
};
export default MachineSelectBox;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./MachineSelect";
Loading

0 comments on commit a698680

Please sign in to comment.