Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(devices): use MainToolbar in DeviceListHeader MAASENG-2515 #5254

Merged
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@canonical/maas-react-components": "1.14.0",
"@canonical/maas-react-components": "1.15.0",
"@canonical/macaroon-bakery": "1.3.2",
"@canonical/react-components": "0.46.0",
"@reduxjs/toolkit": "1.9.3",
Expand Down
11 changes: 3 additions & 8 deletions src/app/devices/views/DeviceList/DeviceList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ describe("DeviceList", () => {
route: "/devices?q=test+search",
state,
});
expect(screen.getByRole("searchbox", { name: "Search" })).toHaveValue(
"test search"
);
expect(screen.getByRole("searchbox")).toHaveValue("test search");
});

it("changes the URL when the search text changes", async () => {
Expand All @@ -42,11 +40,8 @@ describe("DeviceList", () => {
</>,
{ route: "/machines?q=test+search", state }
);
await userEvent.clear(screen.getByRole("searchbox", { name: "Search" }));
await userEvent.type(
screen.getByRole("searchbox", { name: "Search" }),
"hostname:foo"
);
await userEvent.clear(screen.getByRole("searchbox"));
await userEvent.type(screen.getByRole("searchbox"), "hostname:foo");

await waitFor(() => {
expect(search).toBe("?hostname=foo");
Expand Down
3 changes: 1 addition & 2 deletions src/app/devices/views/DeviceList/DeviceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router";
import { useNavigate } from "react-router-dom-v5-compat";

import DeviceListControls from "./DeviceListControls";
import DeviceListHeader from "./DeviceListHeader";
import DeviceListTable from "./DeviceListTable";

Expand Down Expand Up @@ -53,6 +52,7 @@ const DeviceList = (): JSX.Element => {
<PageContent
header={
<DeviceListHeader
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
setSidePanelContent={setSidePanelContent}
/>
Expand All @@ -68,7 +68,6 @@ const DeviceList = (): JSX.Element => {
}
sidePanelTitle={getSidePanelTitle("Devices", sidePanelContent)}
>
<DeviceListControls filter={searchFilter} setFilter={setSearchFilter} />
<DeviceListTable
devices={filteredDevices}
hasFilter={!!searchFilter}
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("DeviceListHeader", () => {
state.device.loaded = false;
renderWithBrowserRouter(
<DeviceListHeader
searchFilter=""
setSearchFilter={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
Expand All @@ -42,42 +43,66 @@ describe("DeviceListHeader", () => {
state.device.loaded = true;
renderWithBrowserRouter(
<DeviceListHeader
searchFilter=""
setSearchFilter={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
{ state }
);
expect(screen.getByTestId("section-header-subtitle")).toHaveTextContent(
"2 devices available"
);
expect(screen.getByText("2 devices available")).toBeInTheDocument();
});

it("disables the add device button if any devices are selected", () => {
state.device.selected = ["abc123"];
renderWithBrowserRouter(
<DeviceListHeader
searchFilter=""
setSearchFilter={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
{ state }
);
expect(screen.getByTestId("add-device-button")).toBeDisabled();
expect(screen.getByRole("button", { name: "Add device" })).toBeDisabled();
});

it("can open the add device form", async () => {
const setSidePanelContent = jest.fn();
renderWithBrowserRouter(
<MemoryRouter>
<DeviceListHeader
searchFilter=""
setSearchFilter={jest.fn()}
setSidePanelContent={setSidePanelContent}
/>
</MemoryRouter>,
{ state }
);
await userEvent.click(screen.getByTestId("add-device-button"));
await userEvent.click(screen.getByRole("button", { name: "Add device" }));
expect(setSidePanelContent).toHaveBeenCalledWith({
view: DeviceSidePanelViews.ADD_DEVICE,
});
});

it("changes the search text when the filters change", () => {
const { rerender } = renderWithBrowserRouter(
<DeviceListHeader
searchFilter=""
setSearchFilter={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
{ state }
);

expect(screen.getByRole("searchbox")).toHaveValue("");

rerender(
<DeviceListHeader
searchFilter="free-text"
setSearchFilter={jest.fn()}
setSidePanelContent={jest.fn()}
/>
);

expect(screen.getByRole("searchbox")).toHaveValue("free-text");
});
});
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
import { Button } from "@canonical/react-components";
import { useEffect, useState } from "react";

import { MainToolbar } from "@canonical/maas-react-components";
import { Button, Col, Spinner } from "@canonical/react-components";
import { useSelector } from "react-redux";

import DeviceFilterAccordion from "./DeviceFilterAccordion";

import DebounceSearchBox from "app/base/components/DebounceSearchBox";
import ModelListSubtitle from "app/base/components/ModelListSubtitle";
import NodeActionMenu from "app/base/components/NodeActionMenu";
import SectionHeader from "app/base/components/SectionHeader";
import type { SetSearchFilter } from "app/base/types";
import { DeviceSidePanelViews } from "app/devices/constants";
import type { DeviceSetSidePanelContent } from "app/devices/types";
import deviceSelectors from "app/store/device/selectors";

type Props = {
searchFilter: string;
setSidePanelContent: DeviceSetSidePanelContent;
setSearchFilter: SetSearchFilter;
};

const DeviceListHeader = ({
searchFilter,
setSidePanelContent,
setSearchFilter,
}: Props): JSX.Element => {
const devices = useSelector(deviceSelectors.all);
const devicesLoaded = useSelector(deviceSelectors.loaded);
const selectedDevices = useSelector(deviceSelectors.selected);
const [searchText, setSearchText] = useState(searchFilter);

useEffect(() => {
// If the filters change then update the search input text.
setSearchText(searchFilter);
}, [searchFilter]);

return (
<SectionHeader
buttons={[
<MainToolbar>
<MainToolbar.Title data-testid="section-header-title">
Devices
</MainToolbar.Title>
{devicesLoaded ? (
<ModelListSubtitle
available={devices.length}
filterSelected={() => setSearchFilter("in:(Selected)")}
modelName="device"
selected={selectedDevices.length}
/>
) : (
<Spinner text="Loading" />
)}
<MainToolbar.Controls>
<Col size={3}>
<DeviceFilterAccordion
searchText={searchText}
setSearchText={setSearchFilter}
/>
</Col>
<DebounceSearchBox
onDebounced={(debouncedText) => setSearchFilter(debouncedText)}
searchText={searchText}
setSearchText={setSearchText}
/>
<Button
data-testid="add-device-button"
disabled={selectedDevices.length > 0}
Expand All @@ -33,7 +70,7 @@ const DeviceListHeader = ({
}
>
Add device
</Button>,
</Button>
<NodeActionMenu
filterActions
hasSelection={selectedDevices.length > 0}
Expand All @@ -48,19 +85,9 @@ const DeviceListHeader = ({
}
}}
showCount
/>,
]}
subtitle={
<ModelListSubtitle
available={devices.length}
filterSelected={() => setSearchFilter("in:(Selected)")}
modelName="device"
selected={selectedDevices.length}
/>
}
subtitleLoading={!devicesLoaded}
title="Devices"
/>
</MainToolbar.Controls>
</MainToolbar>
);
};

Expand Down
8 changes: 8 additions & 0 deletions src/scss/_vanilla-overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@
.p-tooltip__message {
z-index: $side-navigation-z-index + 1;
}

// 05-01-2024 Nick: Using MainToolbar without ContentSection means no padding at top of page.
// Will be fixed once all main pages use MainToolbar and ContentSection is implemented
// https://warthogs.atlassian.net/browse/MAASENG-2440
// https://github.com/canonical/maas-ui/pull/5244
.main-toolbar {
padding-top: $spv--medium;
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2907,10 +2907,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==

"@canonical/maas-react-components@1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@canonical/maas-react-components/-/maas-react-components-1.14.0.tgz#3347a28cc968ed8a13a5e501ec6ebde11a0274c5"
integrity sha512-ApSpDchiCn8EJ597Er/yMeAzc4+iuphU0eXCmmZVcMFCSl2iQqJ6NncA1eCRPgBN7QQs++wqHJ5ry0VPHup0vA==
"@canonical/maas-react-components@1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@canonical/maas-react-components/-/maas-react-components-1.15.0.tgz#c14ec75eeb2bfe2757e45fb3bebf67689865e4df"
integrity sha512-0KQ0CXi6D2PURVUerRR/0CAitp3Qf7olUIJwXZnrePlLyas9zmnZ2HyVydlS/z5YptaHExqh/EQT9AGln/3T+A==

"@canonical/macaroon-bakery@1.3.2":
version "1.3.2"
Expand Down