Skip to content

Commit

Permalink
[web] Add settings and volumes to proposal page
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Apr 13, 2023
1 parent df85b3c commit 67450cc
Show file tree
Hide file tree
Showing 10 changed files with 1,421 additions and 267 deletions.
2 changes: 1 addition & 1 deletion web/src/components/core/Page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { Icon, Title, PageIcon, MainActions } from "~/components/layout";
* >
* <UserSectionContent />
* </Page>
*
*
* @param {object} props
* @param {string} props.icon - The icon for the page
* @param {string} props.title - The title for the page
Expand Down
6 changes: 6 additions & 0 deletions web/src/components/layout/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import React from 'react';

// NOTE: "@icons" is an alias to use a shorter path to real icons location.
// Check the tsconfig.json file to see its value.
import AddAPhoto from "@icons/add_a_photo.svg?component";
import AutoMode from "@icons/auto_mode.svg?component";
import Apps from "@icons/apps.svg?component";
import Badge from "@icons/badge.svg?component";
import CheckCircle from "@icons/check_circle.svg?component";
Expand Down Expand Up @@ -55,13 +57,16 @@ import SignalCellularAlt from "@icons/signal_cellular_alt.svg?component";
import TaskAlt from "@icons/task_alt.svg?component";
import Terminal from "@icons/terminal.svg?component";
import Translate from "@icons/translate.svg?component";
import Tune from "@icons/tune.svg?component";
import Warning from "@icons/warning.svg?component";
import Wifi from "@icons/wifi.svg?component";
import WifiFind from "@icons/wifi_find.svg?component";

import Loading from "./three-dots-loader-icon.svg?component";

const icons = {
add_a_photo: AddAPhoto,
auto_mode: AutoMode,
apps: Apps,
badge: Badge,
check_circle: CheckCircle,
Expand Down Expand Up @@ -95,6 +100,7 @@ const icons = {
task_alt: TaskAlt,
terminal: Terminal,
translate: Translate,
tune: Tune,
warning: Warning,
wifi: Wifi,
wifi_find: WifiFind
Expand Down
90 changes: 84 additions & 6 deletions web/src/components/storage/ProposalActionsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,93 @@
* find current contact information at www.suse.com.
*/

import React from "react";
import React, { useState } from "react";
import {
List,
ListItem,
ExpandableSection,
Skeleton,
Text
} from "@patternfly/react-core";

import { Section } from "~/components/core";
import { ProposalActions } from "~/components/storage";
import { If, Section } from "~/components/core";

const ProposalActions = ({ actions = [] }) => {
const [isExpanded, setIsExpanded] = useState(false);

// TODO: would be nice adding an aria-description to these lists, but aria-description still in
// draft yet and aria-describedby should be used... which id not ideal right now
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description
const ActionsList = ({ actions }) => {
// Some actions (e.g., deleting a LV) are reported as several actions joined by a line break
const actionItems = (action, id) => {
return action.text.split("\n").map((text, index) => {
return (
<ListItem key={`${id}-${index}`} className={action.delete ? "proposal-action--delete" : null}>
{text}
</ListItem>
);
});
};

const items = actions.map(actionItems).flat();

return <List className="proposal-actions">{items}</List>;
};

if (actions.length === 0) return null;

const generalActions = actions.filter(a => !a.subvol);
const subvolActions = actions.filter(a => a.subvol);
const userAction = isExpanded ? "Hide" : "Show";
const toggleText = `${userAction} ${subvolActions.length} subvolumes actions`;

return (
<>
<Text>
Actions to perform for creating the file systems and for ensuring the system boots.
</Text>
<ActionsList actions={generalActions} />
{subvolActions.length > 0 && (
<ExpandableSection
isIndented
isExpanded={isExpanded}
onToggle={() => setIsExpanded(!isExpanded)}
toggleText={toggleText}
className="expandable-actions"
>
<ActionsList actions={subvolActions} />
</ExpandableSection>
)}
</>
);
};

/**
* @todo Create a component for rendering a customized skeleton
*/
const ActionsSkeleton = () => {
return (
<>
<Skeleton width="80%" />
<Skeleton width="65%" />
<Skeleton width="70%" />
<Skeleton width="65%" />
<Skeleton width="40%" />
</>
);
};

export default function ProposalActionsSection({ actions = [], errors = [], isLoading = false }) {
if (isLoading) errors = [];

export default function ProposalActionsSection({ proposal, errors }) {
return (
<Section title="Result" errors={errors}>
<ProposalActions actions={proposal.result.actions} />
<Section title="Planned Actions" errors={errors}>
<If
condition={isLoading}
then={<ActionsSkeleton />}
else={<ProposalActions actions={actions} />}
/>
</Section>
);
}
113 changes: 105 additions & 8 deletions web/src/components/storage/ProposalActionsSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,115 @@
*/

import React from "react";
import { screen } from "@testing-library/react";
import { installerRender, mockComponent } from "~/test-utils";
import { screen, within, waitForElementToBeRemoved } from "@testing-library/react";
import { mockComponent, plainRender } from "~/test-utils";
import { ProposalActionsSection } from "~/components/storage";

jest.mock("~/components/storage/ProposalActions", () => mockComponent("ProposalActions content"));
jest.mock("@patternfly/react-core", () => {
const original = jest.requireActual("@patternfly/react-core");

const proposal = { result: {} };
return {
...original,
Skeleton: mockComponent("PFSkeleton")
};
});

const actions = [
{ text: 'Create GPT on /dev/vdc', subvol: false, delete: false },
{ text: 'Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition', subvol: false, delete: false },
{ text: 'Create encrypted partition /dev/vdc2 (29.99 GiB) as LVM physical volume', subvol: false, delete: false },
{ text: 'Create volume group system0 (29.98 GiB) with /dev/mapper/cr_vdc2 (29.99 GiB)', subvol: false, delete: false },
{ text: 'Create LVM logical volume /dev/system0/root (20.00 GiB) on volume group system0 for / with btrfs', subvol: false, delete: false },
];

const subvolumeActions = [
{ text: 'Create subvolume @ on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/var on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/usr/local on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/srv on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/root on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/opt on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/home on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/boot/writable on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/boot/grub2/x86_64-efi on /dev/system0/root (20.00 GiB)', subvol: true, delete: false },
{ text: 'Create subvolume @/boot/grub2/i386-pc on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }
];

const destructiveAction = { text: 'Delete ext4 on /dev/vdc', subvol: false, delete: true };

it("renders skeleton while loading", () => {
plainRender(<ProposalActionsSection isLoading actions={actions} />);

screen.getAllByText(/PFSkeleton/);
});

it("renders nothing when there is no actions", () => {
plainRender(<ProposalActionsSection actions={[]} />);

expect(screen.queryByText(/Actions to perform/)).toBeNull();
expect(screen.queryAllByText(/Create/)).toEqual([]);
});

describe("when there are actions", () => {
it("renders an explanatory text", () => {
plainRender(<ProposalActionsSection actions={actions} />);

screen.getByText(/Actions to perform/);
});

it("renders the list of actions", () => {
plainRender(<ProposalActionsSection actions={actions} />);

const actionsList = screen.getByRole("list");
const actionsListItems = within(actionsList).getAllByRole("listitem");
expect(actionsListItems.map(i => i.textContent)).toEqual(actions.map(a => a.text));
});

describe("when there is a destructive action", () => {
it("emphasizes the action", () => {
plainRender(<ProposalActionsSection actions={[destructiveAction, ...actions]} />);

// https://stackoverflow.com/a/63080940
const actionItems = screen.getAllByRole("listitem");
const destructiveActionItem = actionItems.find(item => item.textContent === destructiveAction.text);

expect(destructiveActionItem).toHaveClass("proposal-action--delete");
});
});

describe("when there are subvolume actions", () => {
it("does not render the subvolume actions", () => {
plainRender(<ProposalActionsSection actions={[...actions, ...subvolumeActions]} />);

// For now, we know that there are two lists and the subvolume list is the second one.
// The test could be simplified once we have aria-descriptions for the lists.
const [genericList, subvolList] = screen.getAllByRole("list", { hidden: true });
expect(genericList).not.toBeNull();
expect(subvolList).not.toBeNull();
const subvolItems = within(subvolList).queryAllByRole("listitem");
expect(subvolItems).toEqual([]);
});

it("renders the subvolume actions after clicking on 'show subvolumes'", async () => {
const { user } = plainRender(
<ProposalActionsSection actions={[...actions, ...subvolumeActions]} />
);

const link = screen.getByText(/Show.*subvolumes actions/);

expect(screen.getAllByRole("list").length).toEqual(1);

await user.click(link);

waitForElementToBeRemoved(link);
screen.getByText(/Hide.*subvolumes actions/);

describe("ProposalActionsSection", () => {
it("renders the proposal actions", async () => {
installerRender(<ProposalActionsSection proposal={proposal} />);
// For now, we know that there are two lists and the subvolume list is the second one.
// The test could be simplified once we have aria-descriptions for the lists.
const [, subvolList] = screen.getAllByRole("list");
const subvolItems = within(subvolList).getAllByRole("listitem");

screen.getByText("ProposalActions content");
expect(subvolItems.map(i => i.textContent)).toEqual(subvolumeActions.map(a => a.text));
});
});
});
Loading

0 comments on commit 67450cc

Please sign in to comment.