Skip to content

Commit

Permalink
feat(ui): build filesystems list in machine storage (canonical#1854)
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb Ellis authored Nov 9, 2020
1 parent 15cec8f commit 5a587c7
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 33 deletions.
54 changes: 28 additions & 26 deletions ui/.betterer.results

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions ui/src/app/machines/views/MachineDetails/MachineDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { machine as machineActions } from "app/base/actions";
import MachineHeader from "./MachineHeader";
import MachineNotifications from "./MachineNotifications";
import machineSelectors from "app/store/machine/selectors";
import MachineStorage from "./MachineStorage";
import MachineSummary, { SelectedAction } from "./MachineSummary";
import Section from "app/base/components/Section";
import type { RootState } from "app/store/root/types";
Expand Down Expand Up @@ -56,6 +57,9 @@ const MachineDetails = (): JSX.Element => {
<Route exact path="/machine/:id/summary">
<MachineSummary setSelectedAction={setSelectedAction} />
</Route>
<Route exact path="/machine/:id/storage">
<MachineStorage />
</Route>
<Route exact path="/machine/:id">
<Redirect to={`/machine/${id}/summary`} />
</Route>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { mount } from "enzyme";
import React from "react";

import {
machineDisk as diskFactory,
machineFilesystem as fsFactory,
machinePartition as partitionFactory,
} from "testing/factories";
import FilesystemsTable from "./FilesystemsTable";

describe("FilesystemsTable", () => {
it("can show an empty message", () => {
const wrapper = mount(
<FilesystemsTable disks={[]} specialFilesystems={[]} />
);

expect(wrapper.find("TableRow TableCell").at(0).text()).toBe(
"No filesystems defined."
);
});

it("can show filesystems associated with disks", () => {
const disks = [
diskFactory({
filesystem: fsFactory({ mount_point: "/disk-fs/path" }),
name: "disk-fs",
partitions: [],
}),
];
const wrapper = mount(
<FilesystemsTable disks={disks} specialFilesystems={[]} />
);

expect(wrapper.find("TableRow TableCell").at(0).text()).toBe("disk-fs");
expect(wrapper.find("TableRow TableCell").at(3).text()).toBe(
"/disk-fs/path"
);
});

it("can show filesystems associated with partitions", () => {
const disks = [
diskFactory({
filesystem: null,
partitions: [
partitionFactory({
filesystem: fsFactory({ mount_point: "/partition-fs/path" }),
name: "partition-fs",
}),
],
}),
];
const wrapper = mount(
<FilesystemsTable disks={disks} specialFilesystems={[]} />
);

expect(wrapper.find("TableRow TableCell").at(0).text()).toBe(
"partition-fs"
);
expect(wrapper.find("TableRow TableCell").at(3).text()).toBe(
"/partition-fs/path"
);
});

it("can show special filesystems", () => {
const specialFilesystems = [
fsFactory({ mount_point: "/special-fs/path", fstype: "tmpfs" }),
];
const wrapper = mount(
<FilesystemsTable disks={[]} specialFilesystems={specialFilesystems} />
);

expect(wrapper.find("TableRow TableCell").at(0).text()).toBe("—");
expect(wrapper.find("TableRow TableCell").at(3).text()).toBe(
"/special-fs/path"
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { MainTable } from "@canonical/react-components";
import React from "react";

import { formatBytes } from "app/utils";
import type { Disk, Filesystem, Partition } from "app/store/machine/types";

type FilesystemDetails = {
fstype: Filesystem["fstype"];
id: Filesystem["id"];
mountOptions: Filesystem["mount_options"];
mountPoint: Filesystem["mount_point"];
name: string;
size: number;
};

type Props = {
disks: Disk[];
specialFilesystems: Filesystem[];
};

/**
* Returns whether a storage device has a mounted filesystem. If a filesystem is
* unmounted, it will show in the "Available disks and partitions" table.
* @param storageDevice - the storage device to check.
* @returns whether the storage device has a mounted filesystem.
*/
const hasMountedFilesystem = (storageDevice: Disk | Partition) =>
!!storageDevice?.filesystem?.mount_point;

/**
* Formats a filesystem for use in the filesystems table.
* @param filesystem - the base filesystem object.
* @param name - the name to give the filesystem.
* @param size - the size to give the filesystem.
* @returns formatted filesystem object.
*/
const formatFilesystem = (
filesystem: Filesystem,
name: string,
size: number
): FilesystemDetails => ({
fstype: filesystem.fstype,
id: filesystem.id,
mountPoint: filesystem.mount_point,
mountOptions: filesystem.mount_options,
name,
size,
});

/**
* Returns a combined list of special filesystems, and filesystems associated
* with disks and partitions.
* @param disks - disks to check for filesystems
* @param specialFilesystems - tmpfs or ramfs filesystems
* @returns list of filesystems with extra details for use in table
*/
const getFilesystems = (
disks: Disk[],
specialFilesystems: Filesystem[]
): FilesystemDetails[] => {
const filesystems = disks.reduce(
(diskFilesystems: FilesystemDetails[], disk: Disk) => {
if (hasMountedFilesystem(disk)) {
diskFilesystems.push(
formatFilesystem(disk.filesystem, disk.name, disk.size)
);
}

if (disk.partitions) {
disk.partitions.forEach((partition) => {
if (hasMountedFilesystem(partition)) {
diskFilesystems.push(
formatFilesystem(
partition.filesystem,
partition.name,
partition.size
)
);
}
});
}

return diskFilesystems;
},
[]
);

specialFilesystems.forEach((fs) => {
filesystems.push(formatFilesystem(fs, "—", 0));
});

return filesystems;
};

const FilesystemsTable = ({
disks,
specialFilesystems,
}: Props): JSX.Element => {
const filesystems = getFilesystems(disks, specialFilesystems);

return (
<MainTable
defaultSort="name"
defaultSortDirection="ascending"
headers={[
{
content: "Name",
sortKey: "name",
},
{
content: "Size",
sortKey: "size",
},
{
content: "Filesystem",
sortKey: "fstype",
},
{
content: "Mount point",
sortKey: "mountPoint",
},
{
content: "Mount options",
},
{
content: "Actions",
className: "u-align--right",
},
]}
rows={
filesystems.length > 0
? filesystems.map((fs) => {
const size = formatBytes(fs.size, "B");
return {
columns: [
{ content: fs.name },
{
content: fs.size === 0 ? "—" : `${size.value} ${size.unit}`,
},
{ content: fs.fstype },
{ content: fs.mountPoint },
{ content: fs.mountOptions },
{
content: "",
className: "u-align--right",
},
],
key: fs.id,
sortData: {
mountPoint: fs.mountPoint,
name: fs.name,
size: fs.size,
fstype: fs.fstype,
},
};
})
: [{ columns: [{ content: "No filesystems defined." }] }]
}
sortable
/>
);
};

export default FilesystemsTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./FilesystemsTable";
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { mount } from "enzyme";
import { Provider } from "react-redux";
import { MemoryRouter } from "react-router-dom";
import configureStore from "redux-mock-store";
import React from "react";

import {
machineState as machineStateFactory,
rootState as rootStateFactory,
} from "testing/factories";
import MachineStorage from "./MachineStorage";

const mockStore = configureStore();

describe("MachineStorage", () => {
it("displays a spinner if machine is loading", () => {
const state = rootStateFactory({
machine: machineStateFactory({
items: [],
}),
});
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<MemoryRouter
initialEntries={[{ pathname: "/machine/abc123", key: "testKey" }]}
>
<MachineStorage />
</MemoryRouter>
</Provider>
);
expect(wrapper.find("Spinner").exists()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Spinner } from "@canonical/react-components";
import { useSelector } from "react-redux";
import { useParams } from "react-router";
import React from "react";

import { useWindowTitle } from "app/base/hooks";
import type { RouteParams } from "app/base/types";
import machineSelectors from "app/store/machine/selectors";
import type { RootState } from "app/store/root/types";
import FilesystemsTable from "./FilesystemsTable";

const MachineStorage = (): JSX.Element => {
const params = useParams<RouteParams>();
const { id } = params;
const machine = useSelector((state: RootState) =>
machineSelectors.getById(state, id)
);

useWindowTitle(`${`${machine?.fqdn} ` || "Machine"} storage`);

if (machine && "disks" in machine && "special_filesystems" in machine) {
return (
<>
<h4>Filesystems</h4>
<FilesystemsTable
disks={machine.disks}
specialFilesystems={machine.special_filesystems}
/>
</>
);
}
return <Spinner text="Loading..." />;
};

export default MachineStorage;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./MachineStorage";
11 changes: 4 additions & 7 deletions ui/src/app/store/machine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,23 @@ export type NetworkInterface = Model & {
vlan_id: number;
};

export type Filesystem = {
export type Filesystem = Model & {
fstype: string;
label: string;
mount_options: string | null;
mount_point: string;
uuid: string;
used_for: string;
};

export type Partition = Model & {
bootable: boolean;
device_id: number;
filesystem: Filesystem | null;
name: string;
path: string;
resource_uri: string;
size_human: string;
size: number;
system_id: string;
tags: string[];
type: string;
used_for: string;
uuid: string;
};

export type Disk = Model & {
Expand Down
3 changes: 3 additions & 0 deletions ui/src/testing/factories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ export {
machine,
machineDetails,
machineDevice,
machineDisk,
machineEvent,
machineEventType,
machineFilesystem,
machineInterface,
machineNumaNode,
machinePartition,
controller,
pod,
podDetails,
Expand Down
Loading

0 comments on commit 5a587c7

Please sign in to comment.